1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'package:meta/meta.dart'; 6 7import '../application_package.dart'; 8import '../base/file_system.dart'; 9import '../build_info.dart'; 10import '../globals.dart'; 11import '../ios/plist_parser.dart'; 12import '../project.dart'; 13 14/// Tests whether a [FileSystemEntity] is an macOS bundle directory 15bool _isBundleDirectory(FileSystemEntity entity) => 16 entity is Directory && entity.path.endsWith('.app'); 17 18abstract class MacOSApp extends ApplicationPackage { 19 MacOSApp({@required String projectBundleId}) : super(id: projectBundleId); 20 21 /// Creates a new [MacOSApp] from a macOS project directory. 22 factory MacOSApp.fromMacOSProject(MacOSProject project) { 23 return BuildableMacOSApp(project); 24 } 25 26 /// Creates a new [MacOSApp] from an existing app bundle. 27 /// 28 /// `applicationBinary` is the path to the framework directory created by an 29 /// Xcode build. By default, this is located under 30 /// "~/Library/Developer/Xcode/DerivedData/" and contains an executable 31 /// which is expected to start the application and send the observatory 32 /// port over stdout. 33 factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) { 34 final _ExecutableAndId executableAndId = _executableFromBundle(applicationBinary); 35 final Directory applicationBundle = fs.directory(applicationBinary); 36 return PrebuiltMacOSApp( 37 bundleDir: applicationBundle, 38 bundleName: applicationBundle.path, 39 projectBundleId: executableAndId.id, 40 executable: executableAndId.executable, 41 ); 42 } 43 44 /// Look up the executable name for a macOS application bundle. 45 static _ExecutableAndId _executableFromBundle(Directory applicationBundle) { 46 final FileSystemEntityType entityType = fs.typeSync(applicationBundle.path); 47 if (entityType == FileSystemEntityType.notFound) { 48 printError('File "${applicationBundle.path}" does not exist.'); 49 return null; 50 } 51 Directory bundleDir; 52 if (entityType == FileSystemEntityType.directory) { 53 final Directory directory = fs.directory(applicationBundle); 54 if (!_isBundleDirectory(directory)) { 55 printError('Folder "${applicationBundle.path}" is not an app bundle.'); 56 return null; 57 } 58 bundleDir = fs.directory(applicationBundle); 59 } else { 60 printError('Folder "${applicationBundle.path}" is not an app bundle.'); 61 return null; 62 } 63 final String plistPath = fs.path.join(bundleDir.path, 'Contents', 'Info.plist'); 64 if (!fs.file(plistPath).existsSync()) { 65 printError('Invalid prebuilt macOS app. Does not contain Info.plist.'); 66 return null; 67 } 68 final Map<String, dynamic> propertyValues = PlistParser.instance.parseFile(plistPath); 69 final String id = propertyValues[PlistParser.kCFBundleIdentifierKey]; 70 final String executableName = propertyValues[PlistParser.kCFBundleExecutable]; 71 if (id == null) { 72 printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier'); 73 return null; 74 } 75 final String executable = fs.path.join(bundleDir.path, 'Contents', 'MacOS', executableName); 76 if (!fs.file(executable).existsSync()) { 77 printError('Could not find macOS binary at $executable'); 78 } 79 return _ExecutableAndId(executable, id); 80 } 81 82 @override 83 String get displayName => id; 84 85 String applicationBundle(BuildMode buildMode); 86 87 String executable(BuildMode buildMode); 88} 89 90class PrebuiltMacOSApp extends MacOSApp { 91 PrebuiltMacOSApp({ 92 @required this.bundleDir, 93 @required this.bundleName, 94 @required this.projectBundleId, 95 @required String executable, 96 }) : _executable = executable, 97 super(projectBundleId: projectBundleId); 98 99 final Directory bundleDir; 100 final String bundleName; 101 final String projectBundleId; 102 103 final String _executable; 104 105 @override 106 String get name => bundleName; 107 108 @override 109 String applicationBundle(BuildMode buildMode) => bundleDir.path; 110 111 @override 112 String executable(BuildMode buildMode) => _executable; 113} 114 115class BuildableMacOSApp extends MacOSApp { 116 BuildableMacOSApp(this.project); 117 118 final MacOSProject project; 119 120 @override 121 String get name => 'macOS'; 122 123 @override 124 String applicationBundle(BuildMode buildMode) { 125 final File appBundleNameFile = project.nameFile; 126 if (!appBundleNameFile.existsSync()) { 127 printError('Unable to find app name. ${appBundleNameFile.path} does not exist'); 128 return null; 129 } 130 return fs.path.join( 131 getMacOSBuildDirectory(), 132 'Build', 133 'Products', 134 buildMode == BuildMode.debug ? 'Debug' : 'Release', 135 appBundleNameFile.readAsStringSync().trim()); 136 } 137 138 @override 139 String executable(BuildMode buildMode) { 140 final String directory = applicationBundle(buildMode); 141 if (directory == null) { 142 return null; 143 } 144 final _ExecutableAndId executableAndId = MacOSApp._executableFromBundle(fs.directory(directory)); 145 return executableAndId?.executable; 146 } 147} 148 149class _ExecutableAndId { 150 _ExecutableAndId(this.executable, this.id); 151 152 final String executable; 153 final String id; 154} 155