flutter/packages/flutter_tools/lib/src/macos/application_package.dart
Chris Bracken d272a3ab80
Reland: [macos] add flavor options to tool commands (#119564)
* Reland: [macos] add flavor options to tool commands

Adds --flavor option to flutter run and flutter build. Running against
preexisting devicelab flavor tests for feature parity between macOS,
iOS, and Android.

This relands #118421 by alex-wallen which was reverted in #118858 due to
the following test failures:

The bail-out with "Host and target are the same. Nothing to install."
added in `packages/flutter_tools/lib/src/commands/install.dart`
triggered failures in the following tests, which unconditionally attempt
to install the built app, which is unsupported on desktop since the
host and target are the same:

* https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8791495589540422465/+/u/run_flutter_view_macos__start_up/test_stdout
* https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8791496218824259121/+/u/run_complex_layout_win_desktop__start_up/test_stdout
* https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8791496218165602641/+/u/run_flutter_gallery_win_desktop__start_up/test_stdout

Fixes #64088

* Partial revert: eliminate install check on desktop

The original flavour support patch included a check that triggered a
failure when flutter install is run on desktop OSes. This was
intentional, since the host and target devices are the same and
installation is unnecessary to launch the app on currently-supported
desktop OSes.

Note that Windows UWP apps *do* require installation to run, and we used
to have an install command for those apps, though UWP is no longer
supported.

Since that part of the change was orthogonal to flavour support itself,
I'm reverting that component of the change and we can deal with it
separately if so desired.
2023-01-31 17:37:46 +00:00

198 lines
7.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 '../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 '../xcode_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) {
// projectBundleId is unused for macOS apps. Use a placeholder bundle ID.
return BuildableMacOSApp(project, 'com.example.placeholder');
}
/// 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.
static MacOSApp? fromPrebuiltApp(FileSystemEntity applicationBinary) {
final _BundleInfo? bundleInfo = _executableFromBundle(applicationBinary);
if (bundleInfo == null) {
return null;
}
return PrebuiltMacOSApp(
uncompressedBundle: bundleInfo.uncompressedBundle,
bundleName: bundleInfo.uncompressedBundle.path,
projectBundleId: bundleInfo.id,
executable: bundleInfo.executable,
applicationPackage: applicationBinary,
);
}
/// 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 uncompressedBundle;
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;
}
uncompressedBundle = 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 {
uncompressedBundle = 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(uncompressedBundle.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.kCFBundleExecutableKey] 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(uncompressedBundle.path, 'Contents', 'MacOS', executableName);
if (!globals.fs.file(executable).existsSync()) {
globals.printError('Could not find macOS binary at $executable');
}
return _BundleInfo(executable, id, uncompressedBundle);
}
@override
String get displayName => id;
String? applicationBundle(BuildInfo buildInfo);
String? executable(BuildInfo buildInfo);
}
class PrebuiltMacOSApp extends MacOSApp implements PrebuiltApplicationPackage {
PrebuiltMacOSApp({
required this.uncompressedBundle,
required this.bundleName,
required this.projectBundleId,
required String executable,
required this.applicationPackage,
}) : _executable = executable,
super(projectBundleId: projectBundleId);
/// The uncompressed bundle of the application.
///
/// [MacOSApp.fromPrebuiltApp] will uncompress the application into a temporary
/// directory even when an `.zip` file was used to create the [MacOSApp] instance.
final Directory uncompressedBundle;
final String bundleName;
final String projectBundleId;
final String _executable;
@override
String get name => bundleName;
@override
String? applicationBundle(BuildInfo buildInfo) => uncompressedBundle.path;
@override
String? executable(BuildInfo buildInfo) => _executable;
/// A [File] or [Directory] pointing to the application bundle.
///
/// This can be either a `.zip` file or an uncompressed `.app` directory.
@override
final FileSystemEntity applicationPackage;
}
class BuildableMacOSApp extends MacOSApp {
BuildableMacOSApp(this.project, String projectBundleId): super(projectBundleId: projectBundleId);
final MacOSProject project;
@override
String get name => 'macOS';
@override
String? applicationBundle(BuildInfo buildInfo) {
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',
bundleDirectory(buildInfo),
appBundleNameFile.readAsStringSync().trim());
}
String bundleDirectory(BuildInfo buildInfo) {
return sentenceCase(buildInfo.mode.name) + (buildInfo.flavor != null
? ' ${sentenceCase(buildInfo.flavor!)}'
: '');
}
@override
String? executable(BuildInfo buildInfo) {
final String? directory = applicationBundle(buildInfo);
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.uncompressedBundle);
final Directory uncompressedBundle;
final String executable;
final String id;
}