| // 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 vmService |
| /// 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; |
| } |