mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Support running macOS prebuilt application (#26593)
This commit is contained in:
parent
65a70bc7d3
commit
1b21d69de4
|
@ -18,6 +18,7 @@ import 'build_info.dart';
|
|||
import 'globals.dart';
|
||||
import 'ios/ios_workflow.dart';
|
||||
import 'ios/plist_utils.dart' as plist;
|
||||
import 'macos/application_package.dart';
|
||||
import 'project.dart';
|
||||
import 'tester/flutter_tester.dart';
|
||||
|
||||
|
@ -42,6 +43,9 @@ class ApplicationPackageFactory {
|
|||
case TargetPlatform.tester:
|
||||
return FlutterTesterApp.fromCurrentDirectory();
|
||||
case TargetPlatform.darwin_x64:
|
||||
return applicationBinary != null
|
||||
? MacOSApp.fromPrebuiltApp(applicationBinary)
|
||||
: null;
|
||||
case TargetPlatform.linux_x64:
|
||||
case TargetPlatform.windows_x64:
|
||||
case TargetPlatform.fuchsia:
|
||||
|
|
|
@ -7,6 +7,7 @@ import '../base/process.dart';
|
|||
|
||||
const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
|
||||
const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
|
||||
const String kCFBundleExecutable = 'CFBundleExecutable';
|
||||
|
||||
// Prefer using [iosWorkflow.getPlistValueFromFile] to enable mocking.
|
||||
String getValueFromFile(String plistFilePath, String key) {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../globals.dart';
|
||||
import '../ios/plist_utils.dart' as plist;
|
||||
|
||||
/// Tests whether a [FileSystemEntity] is an macOS bundle directory
|
||||
bool _isBundleDirectory(FileSystemEntity entity) =>
|
||||
entity is Directory && entity.path.endsWith('.app');
|
||||
|
||||
abstract class MacOSApp extends ApplicationPackage {
|
||||
MacOSApp({@required String projectBundleId}) : super(id: projectBundleId);
|
||||
|
||||
/// Creates a new [MacOSApp] from an existing app bundle.
|
||||
///
|
||||
/// `applicationBinary` is the path to the framework directory created by an
|
||||
/// Xcode build. By default, this is located under
|
||||
/// "~/Library/Developer/Xcode/DerivedData/" and contains an executable
|
||||
/// which is expected to start the application and send the observatory
|
||||
/// port over stdout.
|
||||
factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) {
|
||||
final FileSystemEntityType entityType = fs.typeSync(applicationBinary.path);
|
||||
if (entityType == FileSystemEntityType.notFound) {
|
||||
printError('File "${applicationBinary.path}" does not exist.');
|
||||
return null;
|
||||
}
|
||||
Directory bundleDir;
|
||||
if (entityType == FileSystemEntityType.directory) {
|
||||
final Directory directory = fs.directory(applicationBinary);
|
||||
if (!_isBundleDirectory(directory)) {
|
||||
printError('Folder "${applicationBinary.path}" is not an app bundle.');
|
||||
return null;
|
||||
}
|
||||
bundleDir = fs.directory(applicationBinary);
|
||||
} else {
|
||||
printError('Folder "${applicationBinary.path}" is not an app bundle.');
|
||||
return null;
|
||||
}
|
||||
final String plistPath = fs.path.join(bundleDir.path, 'Contents', 'Info.plist');
|
||||
if (!fs.file(plistPath).existsSync()) {
|
||||
printError('Invalid prebuilt macOS app. Does not contain Info.plist.');
|
||||
return null;
|
||||
}
|
||||
final String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey);
|
||||
final String executableName = plist.getValueFromFile(plistPath, plist.kCFBundleExecutable);
|
||||
if (id == null) {
|
||||
printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier');
|
||||
return null;
|
||||
}
|
||||
final String executable = fs.path.join(bundleDir.path, 'Contents', 'MacOS', executableName);
|
||||
if (!fs.file(executable).existsSync()) {
|
||||
printError('Could not find macOS binary at $executable');
|
||||
return null;
|
||||
}
|
||||
return PrebuiltMacOSApp(
|
||||
bundleDir: bundleDir,
|
||||
bundleName: fs.path.basename(bundleDir.path),
|
||||
projectBundleId: id,
|
||||
executable: executable,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get displayName => id;
|
||||
|
||||
String get executable;
|
||||
}
|
||||
|
||||
class PrebuiltMacOSApp extends MacOSApp {
|
||||
PrebuiltMacOSApp({
|
||||
@required this.bundleDir,
|
||||
@required this.bundleName,
|
||||
@required this.projectBundleId,
|
||||
@required this.executable,
|
||||
}) : super(projectBundleId: projectBundleId);
|
||||
|
||||
final Directory bundleDir;
|
||||
final String bundleName;
|
||||
final String projectBundleId;
|
||||
|
||||
@override
|
||||
final String executable;
|
||||
|
||||
@override
|
||||
String get name => bundleName;
|
||||
}
|
|
@ -2,16 +2,23 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/os.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process_manager.dart';
|
||||
import '../build_info.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart';
|
||||
import '../macos/application_package.dart';
|
||||
import '../protocol_discovery.dart';
|
||||
import 'macos_workflow.dart';
|
||||
|
||||
/// A device that represents a desktop MacOS target.
|
||||
class MacOSDevice extends Device {
|
||||
MacOSDevice() : super('MacOS');
|
||||
MacOSDevice() : super('macOS');
|
||||
|
||||
@override
|
||||
void clearLogs() {}
|
||||
|
@ -19,20 +26,20 @@ class MacOSDevice extends Device {
|
|||
@override
|
||||
DeviceLogReader getLogReader({ApplicationPackage app}) => NoOpDeviceLogReader('macos');
|
||||
|
||||
// Since the host and target devices are the same, no work needs to be done
|
||||
// to install the application.
|
||||
@override
|
||||
Future<bool> installApp(ApplicationPackage app) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
Future<bool> installApp(ApplicationPackage app) async => true;
|
||||
|
||||
// Since the host and target devices are the same, no work needs to be done
|
||||
// to install the application.
|
||||
@override
|
||||
Future<bool> isAppInstalled(ApplicationPackage app) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
Future<bool> isAppInstalled(ApplicationPackage app) async => true;
|
||||
|
||||
// Since the host and target devices are the same, no work needs to be done
|
||||
// to install the application.
|
||||
@override
|
||||
Future<bool> isLatestBuildInstalled(ApplicationPackage app) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true;
|
||||
|
||||
@override
|
||||
Future<bool> get isLocalEmulator async => false;
|
||||
|
@ -41,7 +48,7 @@ class MacOSDevice extends Device {
|
|||
bool isSupported() => true;
|
||||
|
||||
@override
|
||||
String get name => 'MacOS';
|
||||
String get name => 'macOS';
|
||||
|
||||
@override
|
||||
DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();
|
||||
|
@ -50,7 +57,7 @@ class MacOSDevice extends Device {
|
|||
Future<String> get sdkNameAndVersion async => os.name;
|
||||
|
||||
@override
|
||||
Future<LaunchResult> startApp(ApplicationPackage package, {
|
||||
Future<LaunchResult> startApp(covariant MacOSApp package, {
|
||||
String mainPath,
|
||||
String route,
|
||||
DebuggingOptions debuggingOptions,
|
||||
|
@ -59,26 +66,72 @@ class MacOSDevice extends Device {
|
|||
bool applicationNeedsRebuild = false,
|
||||
bool usesTerminalUi = true,
|
||||
bool ipv6 = false,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}) async {
|
||||
if (!prebuiltApplication) {
|
||||
return LaunchResult.failed();
|
||||
}
|
||||
// Stop any running applications with the same executable.
|
||||
await stopApp(package);
|
||||
final Process process = await processManager.start(<String>[package.executable]);
|
||||
final MacOSLogReader logReader = MacOSLogReader(package, process);
|
||||
final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(logReader);
|
||||
try {
|
||||
final Uri observatoryUri = await observatoryDiscovery.uri;
|
||||
return LaunchResult.succeeded(observatoryUri: observatoryUri);
|
||||
} catch (error) {
|
||||
printError('Error waiting for a debug connection: $error');
|
||||
return LaunchResult.failed();
|
||||
} finally {
|
||||
await observatoryDiscovery.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jonahwilliams): implement using process manager.
|
||||
// currently we rely on killing the isolate taking down the application.
|
||||
@override
|
||||
Future<bool> stopApp(ApplicationPackage app) {
|
||||
throw UnimplementedError();
|
||||
Future<bool> stopApp(covariant MacOSApp app) async {
|
||||
final RegExp whitespace = RegExp(r'\s+');
|
||||
bool succeeded = true;
|
||||
try {
|
||||
final ProcessResult result = await processManager.run(<String>[
|
||||
'ps', 'aux',
|
||||
]);
|
||||
if (result.exitCode != 0) {
|
||||
return false;
|
||||
}
|
||||
final List<String> lines = result.stdout.split('\n');
|
||||
for (String line in lines) {
|
||||
if (!line.contains(app.executable)) {
|
||||
continue;
|
||||
}
|
||||
final List<String> values = line.split(whitespace);
|
||||
if (values.length < 2) {
|
||||
continue;
|
||||
}
|
||||
final String pid = values[1];
|
||||
final ProcessResult killResult = await processManager.run(<String>[
|
||||
'kill', pid
|
||||
]);
|
||||
succeeded &= killResult.exitCode == 0;
|
||||
}
|
||||
return true;
|
||||
} on ArgumentError {
|
||||
succeeded = false;
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TargetPlatform> get targetPlatform async => TargetPlatform.darwin_x64;
|
||||
|
||||
// Since the host and target devices are the same, no work needs to be done
|
||||
// to uninstall the application.
|
||||
@override
|
||||
Future<bool> uninstallApp(ApplicationPackage app) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
Future<bool> uninstallApp(ApplicationPackage app) async => true;
|
||||
}
|
||||
|
||||
class MacOSDevices extends PollingDeviceDiscovery {
|
||||
MacOSDevices() : super('macos devices');
|
||||
MacOSDevices() : super('macOS devices');
|
||||
|
||||
@override
|
||||
bool get supportsPlatform => platform.isMacOS;
|
||||
|
@ -99,3 +152,18 @@ class MacOSDevices extends PollingDeviceDiscovery {
|
|||
@override
|
||||
Future<List<String>> getDiagnostics() async => const <String>[];
|
||||
}
|
||||
|
||||
class MacOSLogReader extends DeviceLogReader {
|
||||
MacOSLogReader(this.macOSApp, this.process);
|
||||
|
||||
final MacOSApp macOSApp;
|
||||
final Process process;
|
||||
|
||||
@override
|
||||
Stream<String> get logLines {
|
||||
return process.stdout.transform(utf8.decoder);
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => macOSApp.displayName;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import '../doctor.dart';
|
|||
/// The [MacOSWorkflow] instance.
|
||||
MacOSWorkflow get macOSWorkflow => context[MacOSWorkflow];
|
||||
|
||||
/// The macos-specific implementation of a [Workflow].
|
||||
/// The macOS-specific implementation of a [Workflow].
|
||||
///
|
||||
/// This workflow requires the flutter-desktop-embedding as a sibling
|
||||
/// repository to the flutter repo.
|
||||
|
|
|
@ -2,11 +2,19 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_tools/src/base/context.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/build_info.dart';
|
||||
import 'package:flutter_tools/src/device.dart';
|
||||
import 'package:flutter_tools/src/macos/application_package.dart';
|
||||
import 'package:flutter_tools/src/macos/macos_device.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import '../src/context.dart';
|
||||
|
@ -15,21 +23,79 @@ void main() {
|
|||
group(MacOSDevice, () {
|
||||
final MockPlatform notMac = MockPlatform();
|
||||
final MacOSDevice device = MacOSDevice();
|
||||
final MockProcessManager mockProcessManager = MockProcessManager();
|
||||
when(notMac.isMacOS).thenReturn(false);
|
||||
when(notMac.environment).thenReturn(const <String, String>{});
|
||||
|
||||
test('defaults', () async {
|
||||
expect(await device.targetPlatform, TargetPlatform.darwin_x64);
|
||||
expect(device.name, 'MacOS');
|
||||
when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
|
||||
return ProcessResult(0, 1, '', '');
|
||||
});
|
||||
|
||||
test('unimplemented methods', () {
|
||||
expect(() => device.installApp(null), throwsA(isInstanceOf<UnimplementedError>()));
|
||||
expect(() => device.uninstallApp(null), throwsA(isInstanceOf<UnimplementedError>()));
|
||||
expect(() => device.isLatestBuildInstalled(null), throwsA(isInstanceOf<UnimplementedError>()));
|
||||
expect(() => device.startApp(null), throwsA(isInstanceOf<UnimplementedError>()));
|
||||
expect(() => device.stopApp(null), throwsA(isInstanceOf<UnimplementedError>()));
|
||||
expect(() => device.isAppInstalled(null), throwsA(isInstanceOf<UnimplementedError>()));
|
||||
testUsingContext('defaults', () async {
|
||||
final MockMacOSApp mockMacOSApp = MockMacOSApp();
|
||||
when(mockMacOSApp.executable).thenReturn('foo');
|
||||
expect(await device.targetPlatform, TargetPlatform.darwin_x64);
|
||||
expect(device.name, 'macOS');
|
||||
expect(await device.installApp(mockMacOSApp), true);
|
||||
expect(await device.uninstallApp(mockMacOSApp), true);
|
||||
expect(await device.isLatestBuildInstalled(mockMacOSApp), true);
|
||||
expect(await device.isAppInstalled(mockMacOSApp), true);
|
||||
expect(await device.stopApp(mockMacOSApp), false);
|
||||
}, overrides: <Type, Generator>{
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
|
||||
testUsingContext('stopApp', () async {
|
||||
const String psOut = r'''
|
||||
tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applications/foo
|
||||
''';
|
||||
final MockMacOSApp mockMacOSApp = MockMacOSApp();
|
||||
when(mockMacOSApp.executable).thenReturn('/Applications/foo');
|
||||
when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async {
|
||||
return ProcessResult(1, 0, psOut, '');
|
||||
});
|
||||
when(mockProcessManager.run(<String>['kill', '17193'])).thenAnswer((Invocation invocation) async {
|
||||
return ProcessResult(2, 0, '', '');
|
||||
});
|
||||
expect(await device.stopApp(mockMacOSApp), true);
|
||||
verify(mockProcessManager.run(<String>['kill', '17193']));
|
||||
}, overrides: <Type, Generator>{
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
|
||||
group('startApp', () {
|
||||
final MockMacOSApp macOSApp = MockMacOSApp();
|
||||
final MockFileSystem mockFileSystem = MockFileSystem();
|
||||
final MockProcessManager mockProcessManager = MockProcessManager();
|
||||
final MockFile mockFile = MockFile();
|
||||
final MockProcess mockProcess = MockProcess();
|
||||
when(macOSApp.executable).thenReturn('test');
|
||||
when(mockFileSystem.file('test')).thenReturn(mockFile);
|
||||
when(mockFile.existsSync()).thenReturn(true);
|
||||
when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async {
|
||||
return mockProcess;
|
||||
});
|
||||
when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
|
||||
return ProcessResult(0, 1, '', '');
|
||||
});
|
||||
when(mockProcess.stdout).thenAnswer((Invocation invocation) {
|
||||
return Stream<List<int>>.fromIterable(<List<int>>[
|
||||
utf8.encode('Observatory listening on http://127.0.0.1/0'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('fails without a prebuilt application', () async {
|
||||
final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: false);
|
||||
expect(result.started, false);
|
||||
});
|
||||
|
||||
testUsingContext('Can run from prebuilt application', () async {
|
||||
final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: true);
|
||||
expect(result.started, true);
|
||||
expect(result.observatoryUri, Uri.parse('http://127.0.0.1/0'));
|
||||
}, overrides: <Type, Generator>{
|
||||
FileSystem: () => mockFileSystem,
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
});
|
||||
|
||||
test('noop port forwarding', () async {
|
||||
|
@ -49,3 +115,13 @@ void main() {
|
|||
}
|
||||
|
||||
class MockPlatform extends Mock implements Platform {}
|
||||
|
||||
class MockMacOSApp extends Mock implements MacOSApp {}
|
||||
|
||||
class MockFileSystem extends Mock implements FileSystem {}
|
||||
|
||||
class MockFile extends Mock implements File {}
|
||||
|
||||
class MockProcessManager extends Mock implements ProcessManager {}
|
||||
|
||||
class MockProcess extends Mock implements Process {}
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/macos/macos_workflow.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
import '../src/context.dart';
|
||||
|
@ -20,6 +22,10 @@ void main() {
|
|||
when(macWithFde.isMacOS).thenReturn(true);
|
||||
when(notMac.isMacOS).thenReturn(false);
|
||||
|
||||
final MockProcessManager mockProcessManager = MockProcessManager();
|
||||
when(mockProcessManager.run(any)).thenAnswer((Invocation invocation) async {
|
||||
return ProcessResult(0, 1, '', '');
|
||||
});
|
||||
testUsingContext('Applies to mac platform', () {
|
||||
expect(macOSWorkflow.appliesToHostPlatform, true);
|
||||
}, overrides: <Type, Generator>{
|
||||
|
@ -45,3 +51,5 @@ class MockPlatform extends Mock implements Platform {
|
|||
@override
|
||||
Map<String, String> environment = <String, String>{};
|
||||
}
|
||||
|
||||
class MockProcessManager extends Mock implements ProcessManager {}
|
||||
|
|
Loading…
Reference in a new issue