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 {
case TargetPlatform.tester:
return FlutterTesterApp.fromCurrentDirectory();
case TargetPlatform.darwin_x64:
return applicationBinary != null
? MacOSApp.fromPrebuiltApp(applicationBinary)
: null;
return applicationBinary == null
? MacOSApp.fromMacOSProject((await FlutterProject.current()).macos)
: MacOSApp.fromPrebuiltApp(applicationBinary);
case TargetPlatform.web:
return WebApplicationPackage(await FlutterProject.current());
case TargetPlatform.linux_x64:
......
......@@ -5,13 +5,10 @@
import 'dart:async';
import '../base/common.dart';
import '../base/io.dart';
import '../base/platform.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../cache.dart';
import '../convert.dart';
import '../globals.dart';
import '../macos/build_macos.dart';
import '../project.dart';
import '../runner/flutter_command.dart' show FlutterCommandResult;
import 'build.dart';
......@@ -62,23 +59,7 @@ class BuildMacosCommand extends BuildSubCommand {
if (!flutterProject.macos.existsSync()) {
throwToolExit('No macOS desktop project configured.');
}
final Process process = await processManager.start(<String>[
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');
}
await buildMacOS(flutterProject, buildInfo);
return null;
}
}
......@@ -6,9 +6,9 @@ import 'base/platform.dart';
import 'version.dart';
// 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 {
_flutterDesktopEnabled ??= platform.environment['FLUTTER_DESKTOP_EMBEDDING']?.toLowerCase() == 'true';
_flutterDesktopEnabled ??= platform.environment['ENABLE_FLUTTER_DESKTOP']?.toLowerCase() == 'true';
return _flutterDesktopEnabled && !FlutterVersion.instance.isStable;
}
bool _flutterDesktopEnabled;
......@@ -6,8 +6,12 @@ import 'package:meta/meta.dart';
import '../application_package.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../globals.dart';
import '../ios/plist_utils.dart' as plist;
import '../project.dart';
/// Tests whether a [FileSystemEntity] is an macOS bundle directory
bool _isBundleDirectory(FileSystemEntity entity) =>
......@@ -16,6 +20,11 @@ bool _isBundleDirectory(FileSystemEntity entity) =>
abstract class MacOSApp extends ApplicationPackage {
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.
///
/// `applicationBinary` is the path to the framework directory created by an
......@@ -24,21 +33,33 @@ abstract class MacOSApp extends ApplicationPackage {
/// 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);
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) {
printError('File "${applicationBinary.path}" does not exist.');
printError('File "${applicationBundle.path}" does not exist.');
return null;
}
Directory bundleDir;
if (entityType == FileSystemEntityType.directory) {
final Directory directory = fs.directory(applicationBinary);
final Directory directory = fs.directory(applicationBundle);
if (!_isBundleDirectory(directory)) {
printError('Folder "${applicationBinary.path}" is not an app bundle.');
printError('Folder "${applicationBundle.path}" is not an app bundle.');
return null;
}
bundleDir = fs.directory(applicationBinary);
bundleDir = fs.directory(applicationBundle);
} else {
printError('Folder "${applicationBinary.path}" is not an app bundle.');
printError('Folder "${applicationBundle.path}" is not an app bundle.');
return null;
}
final String plistPath = fs.path.join(bundleDir.path, 'Contents', 'Info.plist');
......@@ -55,20 +76,16 @@ abstract class MacOSApp extends ApplicationPackage {
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,
);
return _ExecutableAndId(executable, id);
}
@override
String get displayName => id;
String get executable;
String applicationBundle(BuildMode buildMode);
String executable(BuildMode buildMode);
}
class PrebuiltMacOSApp extends MacOSApp {
......@@ -76,16 +93,59 @@ class PrebuiltMacOSApp extends MacOSApp {
@required this.bundleDir,
@required this.bundleName,
@required this.projectBundleId,
@required this.executable,
}) : super(projectBundleId: projectBundleId);
@required String executable,
}) : _executable = executable,
super(projectBundleId: projectBundleId);
final Directory bundleDir;
final String bundleName;
final String projectBundleId;
@override
final String executable;
final String _executable;
@override
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';
import '../base/platform.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../cache.dart';
import '../convert.dart';
import '../device.dart';
import '../globals.dart';
import '../macos/application_package.dart';
import '../project.dart';
import '../protocol_discovery.dart';
import 'build_macos.dart';
import 'macos_workflow.dart';
/// A device that represents a desktop MacOS target.
......@@ -66,16 +69,27 @@ class MacOSDevice extends Device {
bool usesTerminalUi = true,
bool ipv6 = false,
}) async {
// Stop any running applications with the same executable.
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);
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 ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(logReader);
try {
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);
} catch (error) {
printError('Error waiting for a debug connection: $error');
......@@ -91,6 +105,8 @@ class MacOSDevice extends Device {
Future<bool> stopApp(covariant MacOSApp app) async {
final RegExp whitespace = RegExp(r'\s+');
bool succeeded = true;
// assume debug for now.
final String executable = app.executable(BuildMode.debug);
try {
final ProcessResult result = await processManager.run(<String>[
'ps', 'aux',
......@@ -100,7 +116,7 @@ class MacOSDevice extends Device {
}
final List<String> lines = result.stdout.split('\n');
for (String line in lines) {
if (!line.contains(app.executable)) {
if (!line.contains(executable)) {
continue;
}
final List<String> values = line.split(whitespace);
......
......@@ -538,6 +538,9 @@ class MacOSProject {
// Note: The build script file exists as a temporary shim.
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
......@@ -550,6 +553,9 @@ class WindowsProject {
// Note: The build script file exists as a temporary shim.
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.
......@@ -562,4 +568,7 @@ class LinuxProject {
// Note: The build script file exists as a temporary shim.
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() {
), throwsA(isInstanceOf<ToolExit>()));
}, overrides: <Type, Generator>{
Platform: () => linuxPlatform,
FileSystem: () => memoryFilesystem,
});
testUsingContext('Linux build fails on non-linux platform', () async {
......
......@@ -13,7 +13,7 @@ void main() {
group(LinuxWorkflow, () {
final MockPlatform linux = MockPlatform();
final MockPlatform linuxWithFde = MockPlatform()
..environment['FLUTTER_DESKTOP_EMBEDDING'] = 'true';
..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
final MockPlatform notLinux = MockPlatform();
when(linux.isLinux).thenReturn(true);
when(linuxWithFde.isLinux).thenReturn(true);
......
......@@ -32,7 +32,7 @@ void main() {
testUsingContext('defaults', () async {
final MockMacOSApp mockMacOSApp = MockMacOSApp();
when(mockMacOSApp.executable).thenReturn('foo');
when(mockMacOSApp.executable(any)).thenReturn('foo');
expect(await device.targetPlatform, TargetPlatform.darwin_x64);
expect(device.name, 'macOS');
expect(await device.installApp(mockMacOSApp), true);
......@@ -49,7 +49,7 @@ void main() {
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(mockMacOSApp.executable(any)).thenReturn('/Applications/foo');
when(mockProcessManager.run(<String>['ps', 'aux'])).thenAnswer((Invocation invocation) async {
return ProcessResult(1, 0, psOut, '');
});
......@@ -68,7 +68,7 @@ tester 17193 0.0 0.2 4791128 37820 ?? S 2:27PM 0:00.09 /Applica
final MockProcessManager mockProcessManager = MockProcessManager();
final MockFile mockFile = MockFile();
final MockProcess mockProcess = MockProcess();
when(macOSApp.executable).thenReturn('test');
when(macOSApp.executable(any)).thenReturn('test');
when(mockFileSystem.file('test')).thenReturn(mockFile);
when(mockFile.existsSync()).thenReturn(true);
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
]);
});
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);
......
......@@ -16,7 +16,7 @@ void main() {
group(MacOSWorkflow, () {
final MockPlatform mac = MockPlatform();
final MockPlatform macWithFde = MockPlatform()
..environment['FLUTTER_DESKTOP_EMBEDDING'] = 'true';
..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
final MockPlatform notMac = MockPlatform();
when(mac.isMacOS).thenReturn(true);
when(macWithFde.isMacOS).thenReturn(true);
......
......@@ -14,7 +14,7 @@ void main() {
group(WindowsWorkflow, () {
final MockPlatform windows = MockPlatform();
final MockPlatform windowsWithFde = MockPlatform()
..environment['FLUTTER_DESKTOP_EMBEDDING'] = 'true';
..environment['ENABLE_FLUTTER_DESKTOP'] = 'true';
final MockPlatform notWindows = MockPlatform();
when(windows.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