mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add run capability for macOS target (#31218)
This commit is contained in:
parent
9fd8caa4a7
commit
da92fc1109
@ -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;
|
||||
}
|
||||
|
||||
43
packages/flutter_tools/lib/src/macos/build_macos.dart
Normal file
43
packages/flutter_tools/lib/src/macos/build_macos.dart
Normal file
@ -0,0 +1,43 @@
|
||||
// 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 {
|
||||
if (!prebuiltApplication) {
|
||||
return LaunchResult.failed();
|
||||
}
|
||||
// Stop any running applications with the same executable.
|
||||
if (!prebuiltApplication) {
|
||||
Cache.releaseLockEarly();
|
||||
await buildMacOS(await FlutterProject.current(), debuggingOptions?.buildInfo);
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user