flutter/packages/flutter_tools/lib/src/macos/application_package.dart
Andy Weiss c4ceea397a
[flutter_tools] Support zipped application bundles for macOS (#68854)
* [flutter_tools] Support zipped application bundles for macOS

It is not possible to directly produce a directory (.app) in some build systems
but rather it must be zip'ed before being passed to the tool for
running. This adds support for attempting to extract an application
bundle from a zip file if the bundle is not already a directory. This
uses very similar code from lib/src/application_package.dart which is
used for extracting an ipa for iOS.

This introduces tests for the macos/application_package.dart behavior which did not exist before. These tests cover the changes in the PR and some of the existing behavior, but do not cover everything in that file.
2020-11-02 08:58:33 -08:00

180 lines
6.0 KiB
Dart

// 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.
import 'package:meta/meta.dart';
import '../application_package.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../globals.dart' as globals;
import '../ios/plist_parser.dart';
import '../project.dart';
/// 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 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
/// 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 _BundleInfo bundleInfo = _executableFromBundle(applicationBinary);
if (bundleInfo == null) {
return null;
}
return PrebuiltMacOSApp(
bundleDir: bundleInfo.bundle,
bundleName: bundleInfo.bundle.path,
projectBundleId: bundleInfo.id,
executable: bundleInfo.executable,
);
}
/// Look up the executable name for a macOS application bundle.
static _BundleInfo _executableFromBundle(FileSystemEntity applicationBundle) {
final FileSystemEntityType entityType = globals.fs.typeSync(applicationBundle.path);
if (entityType == FileSystemEntityType.notFound) {
globals.printError('File "${applicationBundle.path}" does not exist.');
return null;
}
Directory bundleDir;
if (entityType == FileSystemEntityType.directory) {
final Directory directory = globals.fs.directory(applicationBundle);
if (!_isBundleDirectory(directory)) {
globals.printError('Folder "${applicationBundle.path}" is not an app bundle.');
return null;
}
bundleDir = globals.fs.directory(applicationBundle);
} else {
// Try to unpack as a zip.
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_app.');
try {
globals.os.unzip(globals.fs.file(applicationBundle), tempDir);
} on ProcessException {
globals.printError('Invalid prebuilt macOS app. Unable to extract bundle from archive.');
return null;
}
try {
bundleDir = tempDir
.listSync()
.whereType<Directory>()
.singleWhere(_isBundleDirectory);
} on StateError {
globals.printError('Archive "${applicationBundle.path}" does not contain a single app bundle.');
return null;
}
}
final String plistPath = globals.fs.path.join(bundleDir.path, 'Contents', 'Info.plist');
if (!globals.fs.file(plistPath).existsSync()) {
globals.printError('Invalid prebuilt macOS app. Does not contain Info.plist.');
return null;
}
final Map<String, dynamic> propertyValues = globals.plistParser.parseFile(plistPath);
final String id = propertyValues[PlistParser.kCFBundleIdentifierKey] as String;
final String executableName = propertyValues[PlistParser.kCFBundleExecutable] as String;
if (id == null) {
globals.printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier');
return null;
}
if (executableName == null) {
globals.printError('Invalid prebuilt macOS app. Info.plist does not contain bundle executable');
return null;
}
final String executable = globals.fs.path.join(bundleDir.path, 'Contents', 'MacOS', executableName);
if (!globals.fs.file(executable).existsSync()) {
globals.printError('Could not find macOS binary at $executable');
}
return _BundleInfo(executable, id, bundleDir);
}
@override
String get displayName => id;
String applicationBundle(BuildMode buildMode);
String executable(BuildMode buildMode);
}
class PrebuiltMacOSApp extends MacOSApp {
PrebuiltMacOSApp({
@required this.bundleDir,
@required this.bundleName,
@required this.projectBundleId,
@required String executable,
}) : _executable = executable,
super(projectBundleId: projectBundleId);
final Directory bundleDir;
final String bundleName;
final String projectBundleId;
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 File appBundleNameFile = project.nameFile;
if (!appBundleNameFile.existsSync()) {
globals.printError('Unable to find app name. ${appBundleNameFile.path} does not exist');
return null;
}
return globals.fs.path.join(
getMacOSBuildDirectory(),
'Build',
'Products',
toTitleCase(getNameForBuildMode(buildMode)),
appBundleNameFile.readAsStringSync().trim());
}
@override
String executable(BuildMode buildMode) {
final String directory = applicationBundle(buildMode);
if (directory == null) {
return null;
}
final _BundleInfo bundleInfo = MacOSApp._executableFromBundle(globals.fs.directory(directory));
return bundleInfo?.executable;
}
}
class _BundleInfo {
_BundleInfo(this.executable, this.id, this.bundle);
final Directory bundle;
final String executable;
final String id;
}