1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// 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;
}