Unverified Commit da92fc11 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add run capability for macOS target (#31218)

parent 9fd8caa4
...@@ -50,9 +50,9 @@ class ApplicationPackageFactory { ...@@ -50,9 +50,9 @@ class ApplicationPackageFactory {
case TargetPlatform.tester: case TargetPlatform.tester:
return FlutterTesterApp.fromCurrentDirectory(); return FlutterTesterApp.fromCurrentDirectory();
case TargetPlatform.darwin_x64: case TargetPlatform.darwin_x64:
return applicationBinary != null return applicationBinary == null
? MacOSApp.fromPrebuiltApp(applicationBinary) ? MacOSApp.fromMacOSProject((await FlutterProject.current()).macos)
: null; : MacOSApp.fromPrebuiltApp(applicationBinary);
case TargetPlatform.web: case TargetPlatform.web:
return WebApplicationPackage(await FlutterProject.current()); return WebApplicationPackage(await FlutterProject.current());
case TargetPlatform.linux_x64: case TargetPlatform.linux_x64:
......
...@@ -5,13 +5,10 @@ ...@@ -5,13 +5,10 @@
import 'dart:async'; import 'dart:async';
import '../base/common.dart'; import '../base/common.dart';
import '../base/io.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process_manager.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../convert.dart'; import '../macos/build_macos.dart';
import '../globals.dart';
import '../project.dart'; import '../project.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult; import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart'; import 'build.dart';
...@@ -62,23 +59,7 @@ class BuildMacosCommand extends BuildSubCommand { ...@@ -62,23 +59,7 @@ class BuildMacosCommand extends BuildSubCommand {
if (!flutterProject.macos.existsSync()) { if (!flutterProject.macos.existsSync()) {
throwToolExit('No macOS desktop project configured.'); throwToolExit('No macOS desktop project configured.');
} }
final Process process = await processManager.start(<String>[ await buildMacOS(flutterProject, buildInfo);
flutterProject.macos.buildScript.path,
Cache.flutterRoot,
buildInfo.isDebug ? 'debug' : 'release',
], runInShell: true);
process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(printError);
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(printStatus);
final int result = await process.exitCode;
if (result != 0) {
throwToolExit('Build process failed');
}
return null; return null;
} }
} }
...@@ -6,9 +6,9 @@ import 'base/platform.dart'; ...@@ -6,9 +6,9 @@ import 'base/platform.dart';
import 'version.dart'; import 'version.dart';
// Only launch or display desktop embedding devices if // Only launch or display desktop embedding devices if
// `FLUTTER_DESKTOP_EMBEDDING` environment variable is set to true. // `ENABLE_FLUTTER_DESKTOP` environment variable is set to true.
bool get flutterDesktopEnabled { bool get flutterDesktopEnabled {
_flutterDesktopEnabled ??= platform.environment['FLUTTER_DESKTOP_EMBEDDING']?.toLowerCase() == 'true'; _flutterDesktopEnabled ??= platform.environment['ENABLE_FLUTTER_DESKTOP']?.toLowerCase() == 'true';
return _flutterDesktopEnabled && !FlutterVersion.instance.isStable; return _flutterDesktopEnabled && !FlutterVersion.instance.isStable;
} }
bool _flutterDesktopEnabled; bool _flutterDesktopEnabled;
...@@ -6,8 +6,12 @@ import 'package:meta/meta.dart'; ...@@ -6,8 +6,12 @@ import 'package:meta/meta.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/plist_utils.dart' as plist; import '../ios/plist_utils.dart' as plist;
import '../project.dart';
/// Tests whether a [FileSystemEntity] is an macOS bundle directory /// Tests whether a [FileSystemEntity] is an macOS bundle directory
bool _isBundleDirectory(FileSystemEntity entity) => bool _isBundleDirectory(FileSystemEntity entity) =>
...@@ -16,6 +20,11 @@ bool _isBundleDirectory(FileSystemEntity entity) => ...@@ -16,6 +20,11 @@ bool _isBundleDirectory(FileSystemEntity entity) =>
abstract class MacOSApp extends ApplicationPackage { abstract class MacOSApp extends ApplicationPackage {
MacOSApp({@required String projectBundleId}) : super(id: projectBundleId); MacOSApp({@required String projectBundleId}) : super(id: projectBundleId);
/// Creates a new [MacOSApp] from a macOS project directory.
factory MacOSApp.fromMacOSProject(MacOSProject project) {
return BuildableMacOSApp(project);
}
/// Creates a new [MacOSApp] from an existing app bundle. /// Creates a new [MacOSApp] from an existing app bundle.
/// ///
/// `applicationBinary` is the path to the framework directory created by an /// `applicationBinary` is the path to the framework directory created by an
...@@ -24,21 +33,33 @@ abstract class MacOSApp extends ApplicationPackage { ...@@ -24,21 +33,33 @@ abstract class MacOSApp extends ApplicationPackage {
/// which is expected to start the application and send the observatory /// which is expected to start the application and send the observatory
/// port over stdout. /// port over stdout.
factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) { factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) {
final FileSystemEntityType entityType = fs.typeSync(applicationBinary.path); final _ExecutableAndId executableAndId = _executableFromBundle(applicationBinary);
final Directory applicationBundle = fs.directory(applicationBinary);
return PrebuiltMacOSApp(
bundleDir: applicationBundle,
bundleName: applicationBundle.path,
projectBundleId: executableAndId.id,
executable: executableAndId.executable,
);
}
/// Look up the executable name for a macOS application bundle.
static _ExecutableAndId _executableFromBundle(Directory applicationBundle) {
final FileSystemEntityType entityType = fs.typeSync(applicationBundle.path);
if (entityType == FileSystemEntityType.notFound) { if (entityType == FileSystemEntityType.notFound) {
printError('File "${applicationBinary.path}" does not exist.'); printError('File "${applicationBundle.path}" does not exist.');
return null; return null;
} }
Directory bundleDir; Directory bundleDir;
if (entityType == FileSystemEntityType.directory) { if (entityType == FileSystemEntityType.directory) {
final Directory directory = fs.directory(applicationBinary); final Directory directory = fs.directory(applicationBundle);
if (!_isBundleDirectory(directory)) { if (!_isBundleDirectory(directory)) {
printError('Folder "${applicationBinary.path}" is not an app bundle.'); printError('Folder "${applicationBundle.path}" is not an app bundle.');
return null; return null;
} }
bundleDir = fs.directory(applicationBinary); bundleDir = fs.directory(applicationBundle);
} else { } else {
printError('Folder "${applicationBinary.path}" is not an app bundle.'); printError('Folder "${applicationBundle.path}" is not an app bundle.');
return null; return null;
} }
final String plistPath = fs.path.join(bundleDir.path, 'Contents', 'Info.plist'); final String plistPath = fs.path.join(bundleDir.path, 'Contents', 'Info.plist');
...@@ -55,20 +76,16 @@ abstract class MacOSApp extends ApplicationPackage { ...@@ -55,20 +76,16 @@ abstract class MacOSApp extends ApplicationPackage {
final String executable = fs.path.join(bundleDir.path, 'Contents', 'MacOS', executableName); final String executable = fs.path.join(bundleDir.path, 'Contents', 'MacOS', executableName);
if (!fs.file(executable).existsSync()) { if (!fs.file(executable).existsSync()) {
printError('Could not find macOS binary at $executable'); printError('Could not find macOS binary at $executable');
return null;
} }
return PrebuiltMacOSApp( return _ExecutableAndId(executable, id);
bundleDir: bundleDir,
bundleName: fs.path.basename(bundleDir.path),
projectBundleId: id,
executable: executable,
);
} }
@override @override
String get displayName => id; String get displayName => id;
String get executable; String applicationBundle(BuildMode buildMode);
String executable(BuildMode buildMode);
} }
class PrebuiltMacOSApp extends MacOSApp { class PrebuiltMacOSApp extends MacOSApp {
...@@ -76,16 +93,59 @@ class PrebuiltMacOSApp extends MacOSApp { ...@@ -76,16 +93,59 @@ class PrebuiltMacOSApp extends MacOSApp {
@required this.bundleDir, @required this.bundleDir,
@required this.bundleName, @required this.bundleName,
@required this.projectBundleId, @required this.projectBundleId,
@required this.executable, @required String executable,
}) : super(projectBundleId: projectBundleId); }) : _executable = executable,
super(projectBundleId: projectBundleId);
final Directory bundleDir; final Directory bundleDir;
final String bundleName; final String bundleName;
final String projectBundleId; final String projectBundleId;
@override final String _executable;
final String executable;
@override @override
String get name => bundleName; String get name => bundleName;
@override
String applicationBundle(BuildMode buildMode) => bundleDir.path;
@override
String executable(BuildMode buildMode) => _executable;
}
class BuildableMacOSApp extends MacOSApp {
BuildableMacOSApp(this.project);
final MacOSProject project;
@override
String get name => 'macOS';
@override
String applicationBundle(BuildMode buildMode) {
final ProcessResult result = processManager.runSync(<String>[
project.nameScript.path,
buildMode == BuildMode.debug ? 'debug' : 'release'
], runInShell: true);
final String directory = result.stdout.toString().trim();
return directory;
}
@override
String executable(BuildMode buildMode) {
final ProcessResult result = processManager.runSync(<String>[
project.nameScript.path,
buildMode == BuildMode.debug ? 'debug' : 'release'
], runInShell: true);
final String directory = result.stdout.toString().trim();
final _ExecutableAndId executableAndId = MacOSApp._executableFromBundle(fs.directory(directory));
return executableAndId.executable;
}
}
class _ExecutableAndId {
_ExecutableAndId(this.executable, this.id);
final String executable;
final String id;
} }
// 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 '../base/common.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../cache.dart';
import '../convert.dart';
import '../globals.dart';
import '../project.dart';
/// Builds the macOS project through the project shell script.
Future<void> buildMacOS(FlutterProject flutterProject, BuildInfo buildInfo) async {
final Process process = await processManager.start(<String>[
flutterProject.macos.buildScript.path,
Cache.flutterRoot,
buildInfo?.isDebug == true ? 'debug' : 'release',
], runInShell: true);
final Status status = logger.startProgress(
'Building macOS application...',
timeout: null,
);
int result;
try {
process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(printError);
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(printTrace);
result = await process.exitCode;
} finally {
status.cancel();
}
if (result != 0) {
throwToolExit('Build process failed');
}
}
...@@ -8,11 +8,14 @@ import '../base/os.dart'; ...@@ -8,11 +8,14 @@ import '../base/os.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process_manager.dart'; import '../base/process_manager.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart';
import '../convert.dart'; import '../convert.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart'; import '../globals.dart';
import '../macos/application_package.dart'; import '../macos/application_package.dart';
import '../project.dart';
import '../protocol_discovery.dart'; import '../protocol_discovery.dart';
import 'build_macos.dart';
import 'macos_workflow.dart'; import 'macos_workflow.dart';
/// A device that represents a desktop MacOS target. /// A device that represents a desktop MacOS target.
...@@ -66,16 +69,27 @@ class MacOSDevice extends Device { ...@@ -66,16 +69,27 @@ class MacOSDevice extends Device {
bool usesTerminalUi = true, bool usesTerminalUi = true,
bool ipv6 = false, bool ipv6 = false,
}) async { }) async {
// Stop any running applications with the same executable.
if (!prebuiltApplication) { if (!prebuiltApplication) {
return LaunchResult.failed(); Cache.releaseLockEarly();
await buildMacOS(await FlutterProject.current(), debuggingOptions?.buildInfo);
} }
// Stop any running applications with the same executable. // Make sure to call stop app after we've built.
await stopApp(package); await stopApp(package);
final Process process = await processManager.start(<String>[package.executable]); final Process process = await processManager.start(<String>[
package.executable(debuggingOptions?.buildInfo?.mode)
]);
if (debuggingOptions?.buildInfo?.isRelease == true) {
return LaunchResult.succeeded();
}
final MacOSLogReader logReader = MacOSLogReader(package, process); final MacOSLogReader logReader = MacOSLogReader(package, process);
final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(logReader); final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(logReader);
try { try {
final Uri observatoryUri = await observatoryDiscovery.uri; final Uri observatoryUri = await observatoryDiscovery.uri;
// Bring app to foreground.
await processManager.run(<String>[
'open', package.applicationBundle(debuggingOptions?.buildInfo?.mode),
]);
return LaunchResult.succeeded(observatoryUri: observatoryUri); return LaunchResult.succeeded(observatoryUri: observatoryUri);
} catch (error) { } catch (error) {
printError('Error waiting for a debug connection: $error'); printError('Error waiting for a debug connection: $error');
...@@ -91,6 +105,8 @@ class MacOSDevice extends Device { ...@@ -91,6 +105,8 @@ class MacOSDevice extends Device {
Future<bool> stopApp(covariant MacOSApp app) async { Future<bool> stopApp(covariant MacOSApp app) async {
final RegExp whitespace = RegExp(r'\s+'); final RegExp whitespace = RegExp(r'\s+');
bool succeeded = true; bool succeeded = true;
// assume debug for now.
final String executable = app.executable(BuildMode.debug);
try { try {
final ProcessResult result = await processManager.run(<String>[ final ProcessResult result = await processManager.run(<String>[
'ps', 'aux', 'ps', 'aux',
...@@ -100,7 +116,7 @@ class MacOSDevice extends Device { ...@@ -100,7 +116,7 @@ class MacOSDevice extends Device {
} }
final List<String> lines = result.stdout.split('\n'); final List<String> lines = result.stdout.split('\n');
for (String line in lines) { for (String line in lines) {
if (!line.contains(app.executable)) { if (!line.contains(executable)) {
continue; continue;
} }
final List<String> values = line.split(whitespace); final List<String> values = line.split(whitespace);
......
...@@ -538,6 +538,9 @@ class MacOSProject { ...@@ -538,6 +538,9 @@ class MacOSProject {
// Note: The build script file exists as a temporary shim. // Note: The build script file exists as a temporary shim.
File get buildScript => project.directory.childDirectory('macos').childFile('build.sh'); File get buildScript => project.directory.childDirectory('macos').childFile('build.sh');
// Note: The name script file exists as a temporary shim.
File get nameScript => project.directory.childDirectory('macos').childFile('name_output.sh');
} }
/// The Windows sub project /// The Windows sub project
...@@ -550,6 +553,9 @@ class WindowsProject { ...@@ -550,6 +553,9 @@ class WindowsProject {
// Note: The build script file exists as a temporary shim. // Note: The build script file exists as a temporary shim.
File get buildScript => project.directory.childDirectory('windows').childFile('build.bat'); File get buildScript => project.directory.childDirectory('windows').childFile('build.bat');
// Note: The name script file exists as a temporary shim.
File get nameScript => project.directory.childDirectory('windows').childFile('name_output.sh');
} }
/// The Linux sub project. /// The Linux sub project.
...@@ -562,4 +568,7 @@ class LinuxProject { ...@@ -562,4 +568,7 @@ class LinuxProject {
// Note: The build script file exists as a temporary shim. // Note: The build script file exists as a temporary shim.
File get buildScript => project.directory.childDirectory('linux').childFile('build.sh'); File get buildScript => project.directory.childDirectory('linux').childFile('build.sh');
// Note: The name script file exists as a temporary shim.
File get nameScript => project.directory.childDirectory('linux').childFile('name_output.sh');
} }
\ No newline at end of file
...@@ -44,6 +44,7 @@ void main() { ...@@ -44,6 +44,7 @@ void main() {
), throwsA(isInstanceOf<ToolExit>())); ), throwsA(isInstanceOf<ToolExit>()));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Platform: () => linuxPlatform, Platform: () => linuxPlatform,
FileSystem: () => memoryFilesystem,
}); });
testUsingContext('Linux build fails on non-linux platform', () async { testUsingContext('Linux build fails on non-linux platform', () async {
......
...@@ -13,7 +13,7 @@ void main() { ...@@ -13,7 +13,7 @@ void main() {
group(LinuxWorkflow, () { group(LinuxWorkflow, () {
final MockPlatform linux = MockPlatform(); final MockPlatform linux = MockPlatform();
final MockPlatform linuxWithFde = MockPlatform() final MockPlatform linuxWithFde = MockPlatform()
..environment['FLUTTER_DESKTOP_EMBEDDING'] = 'true'; ..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
final MockPlatform notLinux = MockPlatform(); final MockPlatform notLinux = MockPlatform();
when(linux.isLinux).thenReturn(true); when(linux.isLinux).thenReturn(true);
when(linuxWithFde.isLinux).thenReturn(true); when(linuxWithFde.isLinux).thenReturn(true);
......
...@@ -32,7 +32,7 @@ void main() { ...@@ -32,7 +32,7 @@ void main() {
testUsingContext('defaults', () async { testUsingContext('defaults', () async {
final MockMacOSApp mockMacOSApp = MockMacOSApp(); final MockMacOSApp mockMacOSApp = MockMacOSApp();
when(mockMacOSApp.executable).thenReturn('foo'); when(mockMacOSApp.executable(any)).thenReturn('foo');
expect(await device.targetPlatform, TargetPlatform.darwin_x64); expect(await device.targetPlatform, TargetPlatform.darwin_x64);
expect(device.name, 'macOS'); expect(device.name, 'macOS');
expect(await device.installApp(mockMacOSApp), true); expect(await device.installApp(mockMacOSApp), true);
...@@ -49,7 +49,7 @@ void main() { ...@@ -49,7 +49,7 @@ void main() {
tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applications/foo tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applications/foo
'''; ''';
final MockMacOSApp mockMacOSApp = MockMacOSApp(); final MockMacOSApp mockMacOSApp = MockMacOSApp();
when(mockMacOSApp.executable).thenReturn('/Applications/foo'); when(mockMacOSApp.executable(any)).thenReturn('/Applications/foo');
when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async { when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async {
return ProcessResult(1, 0, psOut, ''); return ProcessResult(1, 0, psOut, '');
}); });
...@@ -68,7 +68,7 @@ tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applica ...@@ -68,7 +68,7 @@ tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applica
final MockProcessManager mockProcessManager = MockProcessManager(); final MockProcessManager mockProcessManager = MockProcessManager();
final MockFile mockFile = MockFile(); final MockFile mockFile = MockFile();
final MockProcess mockProcess = MockProcess(); final MockProcess mockProcess = MockProcess();
when(macOSApp.executable).thenReturn('test'); when(macOSApp.executable(any)).thenReturn('test');
when(mockFileSystem.file('test')).thenReturn(mockFile); when(mockFileSystem.file('test')).thenReturn(mockFile);
when(mockFile.existsSync()).thenReturn(true); when(mockFile.existsSync()).thenReturn(true);
when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async { when(mockProcessManager.start(<String>['test'])).thenAnswer((Invocation invocation) async {
...@@ -83,11 +83,6 @@ tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applica ...@@ -83,11 +83,6 @@ tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applica
]); ]);
}); });
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 { testUsingContext('Can run from prebuilt application', () async {
final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: true); final LaunchResult result = await device.startApp(macOSApp, prebuiltApplication: true);
expect(result.started, true); expect(result.started, true);
......
...@@ -16,7 +16,7 @@ void main() { ...@@ -16,7 +16,7 @@ void main() {
group(MacOSWorkflow, () { group(MacOSWorkflow, () {
final MockPlatform mac = MockPlatform(); final MockPlatform mac = MockPlatform();
final MockPlatform macWithFde = MockPlatform() final MockPlatform macWithFde = MockPlatform()
..environment['FLUTTER_DESKTOP_EMBEDDING'] = 'true'; ..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
final MockPlatform notMac = MockPlatform(); final MockPlatform notMac = MockPlatform();
when(mac.isMacOS).thenReturn(true); when(mac.isMacOS).thenReturn(true);
when(macWithFde.isMacOS).thenReturn(true); when(macWithFde.isMacOS).thenReturn(true);
......
...@@ -14,7 +14,7 @@ void main() { ...@@ -14,7 +14,7 @@ void main() {
group(WindowsWorkflow, () { group(WindowsWorkflow, () {
final MockPlatform windows = MockPlatform(); final MockPlatform windows = MockPlatform();
final MockPlatform windowsWithFde = MockPlatform() final MockPlatform windowsWithFde = MockPlatform()
..environment['FLUTTER_DESKTOP_EMBEDDING'] = 'true'; ..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
final MockPlatform notWindows = MockPlatform(); final MockPlatform notWindows = MockPlatform();
when(windows.isWindows).thenReturn(true); when(windows.isWindows).thenReturn(true);
when(windowsWithFde.isWindows).thenReturn(true); when(windowsWithFde.isWindows).thenReturn(true);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment