blob: d86bf9ba2b2d25ee5c59480e520a7a99c74cb671 [file] [log] [blame]
Devon Carew7ae6f7f2016-02-16 18:23:43 -08001// Copyright 2016 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
Devon Carew67046f92016-02-20 22:00:11 -08005import 'dart:async';
Jason Simmons466d1542018-03-12 11:06:32 -07006import 'dart:convert' show json;
Devon Carew67046f92016-02-20 22:00:11 -08007
xster0c8e1812017-02-07 16:59:58 -08008import 'package:meta/meta.dart';
Devon Carew67046f92016-02-20 22:00:11 -08009
10import '../application_package.dart';
Jakob Andersen60c0c3d2017-03-10 13:43:57 +010011import '../base/common.dart';
Alexandre Ardhuin610955f2017-04-08 08:43:19 +020012import '../base/context.dart';
Todd Volkert8bb27032017-01-06 16:51:44 -080013import '../base/file_system.dart';
Chris Brackencdbdafa2018-05-03 19:40:16 -070014import '../base/fingerprint.dart';
Todd Volkert016b5ab2017-01-09 08:37:00 -080015import '../base/io.dart';
Alexandre Ardhuin610955f2017-04-08 08:43:19 +020016import '../base/logger.dart';
xster1e397d32018-02-15 15:16:23 -080017import '../base/os.dart';
Todd Volkert417c2f22017-01-25 16:06:41 -080018import '../base/platform.dart';
Devon Carew7ae6f7f2016-02-16 18:23:43 -080019import '../base/process.dart';
Todd Volkert60b19b22016-11-30 08:42:42 -080020import '../base/process_manager.dart';
xster1e397d32018-02-15 15:16:23 -080021import '../base/utils.dart';
Chinmay Garde66fee3a2016-05-23 12:58:42 -070022import '../build_info.dart';
Devon Carew67046f92016-02-20 22:00:11 -080023import '../globals.dart';
Jakob Andersenb61e1692017-03-23 14:59:12 +010024import '../plugins.dart';
Sigurd Meldgaard2d3a5c72018-07-20 08:00:30 +020025import '../project.dart';
Devon Carew67046f92016-02-20 22:00:11 -080026import '../services.dart';
xster6a494192017-07-12 18:35:08 -070027import 'cocoapods.dart';
xster9d3fb1f2017-05-18 11:26:43 -070028import 'code_signing.dart';
Chris Brackena45e4e92016-09-26 13:25:57 -070029import 'xcodeproj.dart';
Devon Carew67046f92016-02-20 22:00:11 -080030
Chris Bracken7fb78522017-12-15 18:13:10 -080031const int kXcodeRequiredVersionMajor = 9;
Yegor Jbanov23e634a2016-03-10 13:08:58 -080032const int kXcodeRequiredVersionMinor = 0;
Chinmay Gardec5056b92016-02-19 13:19:11 -080033
Todd Volkert8d11f5c2018-03-28 10:58:28 -070034IMobileDevice get iMobileDevice => context[IMobileDevice];
Chris Brackenf8c50ea2018-09-18 10:05:46 -070035PlistBuddy get plistBuddy => context[PlistBuddy];
Todd Volkert8d11f5c2018-03-28 10:58:28 -070036Xcode get xcode => context[Xcode];
Chris Brackenc04f7122017-06-15 19:24:07 -070037
Chris Brackenf8c50ea2018-09-18 10:05:46 -070038class PlistBuddy {
39 const PlistBuddy();
40
41 static const String path = '/usr/libexec/PlistBuddy';
42
43 Future<ProcessResult> run(List<String> args) => processManager.run(<String>[path]..addAll(args));
44}
45
46/// A property list is a key-value representation commonly used for
47/// configuration on macOS/iOS systems.
48class PropertyList {
49 const PropertyList(this.plistPath);
50
51 final String plistPath;
52
53 /// Prints the specified key, or returns null if not present.
54 Future<String> read(String key) async {
55 final ProcessResult result = await _runCommand('Print $key');
56 if (result.exitCode == 0)
57 return result.stdout.trim();
58 return null;
59 }
60
61 /// Adds [key]. Has no effect if the key already exists.
62 Future<void> addString(String key, String value) async {
63 await _runCommand('Add $key string $value');
64 }
65
66 /// Updates [key] with the new [value]. Has no effect if the key does not exist.
67 Future<void> update(String key, String value) async {
68 await _runCommand('Set $key $value');
69 }
70
71 /// Deletes [key].
72 Future<void> delete(String key) async {
73 await _runCommand('Delete $key');
74 }
75
76 /// Deletes the content of the property list and creates a new root of the specified type.
77 Future<void> clearToDict() async {
78 await _runCommand('Clear dict');
79 }
80
81 Future<ProcessResult> _runCommand(String command) async {
82 return await plistBuddy.run(<String>['-c', command, plistPath]);
83 }
84}
85
Chris Brackend6ec71d2017-06-15 19:03:24 -070086class IMobileDevice {
87 const IMobileDevice();
88
89 bool get isInstalled => exitsHappy(<String>['idevice_id', '-h']);
90
91 /// Returns true if libimobiledevice is installed and working as expected.
92 ///
93 /// Older releases of libimobiledevice fail to work with iOS 10.3 and above.
94 Future<bool> get isWorking async {
95 if (!isInstalled)
96 return false;
97
98 // If no device is attached, we're unable to detect any problems. Assume all is well.
99 final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult;
100 if (result.exitCode != 0 || result.stdout.isEmpty)
101 return true;
102
103 // Check that we can look up the names of any attached devices.
104 return await exitsHappyAsync(<String>['idevicename']);
105 }
106
Chris Brackeneba6ceb2017-09-01 10:10:49 -0700107 Future<String> getAvailableDeviceIDs() async {
108 try {
109 final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']);
110 if (result.exitCode != 0)
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200111 throw ToolExit('idevice_id returned an error:\n${result.stderr}');
Chris Brackeneba6ceb2017-09-01 10:10:49 -0700112 return result.stdout;
113 } on ProcessException {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200114 throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
Chris Brackeneba6ceb2017-09-01 10:10:49 -0700115 }
116 }
117
118 Future<String> getInfoForDevice(String deviceID, String key) async {
119 try {
Mihail Slavchevaf0afff2017-10-19 01:25:49 +0300120 final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key, '--simple']);
Chris Brackeneba6ceb2017-09-01 10:10:49 -0700121 if (result.exitCode != 0)
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200122 throw ToolExit('idevice_id returned an error:\n${result.stderr}');
Chris Brackeneba6ceb2017-09-01 10:10:49 -0700123 return result.stdout.trim();
124 } on ProcessException {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200125 throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
Chris Brackeneba6ceb2017-09-01 10:10:49 -0700126 }
127 }
128
Chris Bracken66502132017-06-16 14:33:49 -0700129 /// Starts `idevicesyslog` and returns the running process.
130 Future<Process> startLogger() => runCommand(<String>['idevicesyslog']);
131
Greg Spencer0259be92017-11-17 10:05:21 -0800132 /// Captures a screenshot to the specified outputFile.
Keerti Parthasarathyaecb7d92018-06-12 09:30:10 -0700133 Future<void> takeScreenshot(File outputFile) {
Chris Bracken66502132017-06-16 14:33:49 -0700134 return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]);
135 }
Chris Brackend6ec71d2017-06-15 19:03:24 -0700136}
137
Chris Bracken5d2a4602017-02-01 16:16:33 -0800138class Xcode {
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100139 bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
Chinmay Gardec5056b92016-02-19 13:19:11 -0800140
Devon Carewc3eec6e2016-03-24 13:36:32 -0700141 String _xcodeSelectPath;
Chris Bracken845c1b72017-06-23 16:58:14 -0700142 String get xcodeSelectPath {
143 if (_xcodeSelectPath == null) {
144 try {
Chris Bracken2ebb9e52017-06-23 18:50:27 -0700145 _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim();
Chris Bracken845c1b72017-06-23 16:58:14 -0700146 } on ProcessException {
147 // Ignore: return null below.
148 }
149 }
150 return _xcodeSelectPath;
151 }
Devon Carewc3eec6e2016-03-24 13:36:32 -0700152
Chris Bracken845c1b72017-06-23 16:58:14 -0700153 bool get isInstalled {
154 if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
155 return false;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100156 return xcodeProjectInterpreter.isInstalled;
Chris Bracken845c1b72017-06-23 16:58:14 -0700157 }
Chinmay Gardec5056b92016-02-19 13:19:11 -0800158
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100159 int get majorVersion => xcodeProjectInterpreter.majorVersion;
160
161 int get minorVersion => xcodeProjectInterpreter.minorVersion;
162
163 String get versionText => xcodeProjectInterpreter.versionText;
164
Devon Carewc3eec6e2016-03-24 13:36:32 -0700165 bool _eulaSigned;
Devon Carew16f9e382016-02-21 00:41:14 -0800166 /// Has the EULA been signed?
Chris Bracken845c1b72017-06-23 16:58:14 -0700167 bool get eulaSigned {
168 if (_eulaSigned == null) {
169 try {
170 final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'clang']);
171 if (result.stdout != null && result.stdout.contains('license'))
172 _eulaSigned = false;
173 else if (result.stderr != null && result.stderr.contains('license'))
174 _eulaSigned = false;
175 else
176 _eulaSigned = true;
177 } on ProcessException {
178 _eulaSigned = false;
179 }
180 }
181 return _eulaSigned;
182 }
Devon Carew16f9e382016-02-21 00:41:14 -0800183
Jonah Williams298f4ef2018-03-17 10:57:51 -0700184 bool _isSimctlInstalled;
185
186 /// Verifies that simctl is installed by trying to run it.
187 bool get isSimctlInstalled {
188 if (_isSimctlInstalled == null) {
189 try {
190 // This command will error if additional components need to be installed in
191 // xcode 9.2 and above.
192 final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']);
Jonah Williams91dcfc52018-03-19 10:53:04 -0700193 _isSimctlInstalled = result.stderr == null || result.stderr == '';
Jonah Williams298f4ef2018-03-17 10:57:51 -0700194 } on ProcessException {
195 _isSimctlInstalled = false;
196 }
197 }
198 return _isSimctlInstalled;
199 }
200
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100201 bool get isVersionSatisfactory {
202 if (!xcodeProjectInterpreter.isInstalled)
Devon Carew4ac18682016-03-28 16:20:43 -0700203 return false;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100204 if (majorVersion > kXcodeRequiredVersionMajor)
205 return true;
206 if (majorVersion == kXcodeRequiredVersionMajor)
207 return minorVersion >= kXcodeRequiredVersionMinor;
208 return false;
Chinmay Gardec5056b92016-02-19 13:19:11 -0800209 }
Chris Bracken26895602018-04-25 20:50:16 -0700210
211 Future<RunResult> cc(List<String> args) {
212 return runCheckedAsync(<String>['xcrun', 'cc']..addAll(args));
213 }
214
215 Future<RunResult> clang(List<String> args) {
216 return runCheckedAsync(<String>['xcrun', 'clang']..addAll(args));
217 }
Danny Tuppenyd3f61282018-07-19 10:32:44 +0100218
219 String getSimulatorPath() {
220 if (xcodeSelectPath == null)
221 return null;
222 final List<String> searchPaths = <String>[
223 fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'),
224 ];
225 return searchPaths.where((String p) => p != null).firstWhere(
226 (String p) => fs.directory(p).existsSync(),
227 orElse: () => null,
228 );
229 }
Devon Carew7ae6f7f2016-02-16 18:23:43 -0800230}
Devon Carew67046f92016-02-20 22:00:11 -0800231
Chris Brackenf8c50ea2018-09-18 10:05:46 -0700232/// Sets the Xcode system.
233///
234/// Xcode 10 added a new (default) build system with better performance and
235/// stricter checks. Flutter apps without plugins build fine under the new
236/// system, but it causes build breakages in projects with CocoaPods enabled.
237/// This affects Flutter apps with plugins.
238///
239/// Once Flutter has been updated to be fully compliant with the new build
240/// system, this can be removed.
241//
242// TODO(cbracken): remove when https://github.com/flutter/flutter/issues/20685 is fixed.
243Future<void> setXcodeWorkspaceBuildSystem({
244 @required Directory workspaceDirectory,
245 @required File workspaceSettings,
246 @required bool modern,
247}) async {
248 // If this isn't a workspace, we're not using CocoaPods and can use the new
249 // build system.
250 if (!workspaceDirectory.existsSync())
251 return;
252
253 final PropertyList plist = PropertyList(workspaceSettings.path);
254 if (!workspaceSettings.existsSync()) {
255 workspaceSettings.parent.createSync(recursive: true);
256 await plist.clearToDict();
257 }
258
259 const String kBuildSystemType = 'BuildSystemType';
260 if (modern) {
261 printTrace('Using new Xcode build system.');
262 await plist.delete(kBuildSystemType);
263 } else {
264 printTrace('Using legacy Xcode build system.');
265 if (await plist.read(kBuildSystemType) == null) {
266 await plist.addString(kBuildSystemType, 'Original');
267 } else {
268 await plist.update(kBuildSystemType, 'Original');
269 }
270 }
271}
272
Adam Barth612a0972016-06-02 08:57:13 -0700273Future<XcodeBuildResult> buildXcodeProject({
Todd Volkert904d5242016-10-13 16:17:50 -0700274 BuildableIOSApp app,
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200275 BuildInfo buildInfo,
Simon Lightfootbe6501a2018-05-21 11:54:38 +0100276 String targetOverride,
Adam Barth612a0972016-06-02 08:57:13 -0700277 bool buildForDevice,
Alexandre Ardhuin09276be2018-06-05 08:50:40 +0200278 bool codesign = true,
279 bool usesTerminalUi = true,
Adam Barth612a0972016-06-02 08:57:13 -0700280}) async {
Mikkel Nygaard Ravn22832d32018-08-30 16:18:44 +0200281 if (!await upgradePbxProjWithFlutterAssets(app.project))
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200282 return XcodeBuildResult(success: false);
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100283
Devon Carew67046f92016-02-20 22:00:11 -0800284 if (!_checkXcodeVersion())
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200285 return XcodeBuildResult(success: false);
Devon Carew67046f92016-02-20 22:00:11 -0800286
Chris Brackenf8c50ea2018-09-18 10:05:46 -0700287 // TODO(cbracken) remove when https://github.com/flutter/flutter/issues/20685 is fixed.
288 await setXcodeWorkspaceBuildSystem(
289 workspaceDirectory: app.project.xcodeWorkspace,
290 workspaceSettings: app.project.xcodeWorkspaceSharedSettings,
291 modern: false,
292 );
293
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200294 final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path);
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200295 if (!projectInfo.targets.contains('Runner')) {
296 printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
297 printError('Open Xcode to fix the problem:');
298 printError(' open ios/Runner.xcworkspace');
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200299 return XcodeBuildResult(success: false);
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200300 }
301 final String scheme = projectInfo.schemeFor(buildInfo);
302 if (scheme == null) {
303 printError('');
304 if (projectInfo.definesCustomSchemes) {
305 printError('The Xcode project defines schemes: ${projectInfo.schemes.join(', ')}');
306 printError('You must specify a --flavor option to select one of them.');
307 } else {
308 printError('The Xcode project does not define custom schemes.');
309 printError('You cannot use the --flavor option.');
310 }
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200311 return XcodeBuildResult(success: false);
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200312 }
313 final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme);
314 if (configuration == null) {
315 printError('');
316 printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}');
317 printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.');
318 printError('Open Xcode to fix the problem:');
319 printError(' open ios/Runner.xcworkspace');
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200320 return XcodeBuildResult(success: false);
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200321 }
322
xster45653952018-03-28 18:58:06 -0700323 Map<String, String> autoSigningConfigs;
xsterc2b0a302017-06-07 15:56:13 -0700324 if (codesign && buildForDevice)
xster45653952018-03-28 18:58:06 -0700325 autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi);
xsterb232a842017-05-15 12:54:32 -0700326
Devon Carew67046f92016-02-20 22:00:11 -0800327 // Before the build, all service definitions must be updated and the dylibs
328 // copied over to a location that is suitable for Xcodebuild to find them.
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200329 await _addServicesToBundle(app.project.hostAppRoot);
Devon Carew67046f92016-02-20 22:00:11 -0800330
Mikkel Nygaard Ravnb2800742018-08-02 14:12:25 +0200331 final FlutterProject project = await FlutterProject.current();
Sigurd Meldgaard2d3a5c72018-07-20 08:00:30 +0200332 await updateGeneratedXcodeProperties(
333 project: project,
Simon Lightfootbe6501a2018-05-21 11:54:38 +0100334 targetOverride: targetOverride,
Sigurd Meldgaard2d3a5c72018-07-20 08:00:30 +0200335 buildInfo: buildInfo,
xster837f1012017-05-04 12:26:58 -0700336 );
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200337 refreshPluginsList(project);
338 if (hasPlugins(project) || (project.isModule && project.ios.podfile.existsSync())) {
Chris Brackencdbdafa2018-05-03 19:40:16 -0700339 // If the Xcode project, Podfile, or Generated.xcconfig have changed since
340 // last run, pods should be updated.
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200341 final Fingerprinter fingerprinter = Fingerprinter(
Chris Brackencdbdafa2018-05-03 19:40:16 -0700342 fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'),
343 paths: <String>[
Mikkel Nygaard Ravn22832d32018-08-30 16:18:44 +0200344 app.project.xcodeProjectInfoFile.path,
345 app.project.podfile.path,
346 app.project.generatedXcodePropertiesFile.path,
Chris Brackencdbdafa2018-05-03 19:40:16 -0700347 ],
348 properties: <String, String>{},
349 );
350 final bool didPodInstall = await cocoaPods.processPods(
Sigurd Meldgaard2d3a5c72018-07-20 08:00:30 +0200351 iosProject: project.ios,
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +0100352 iosEngineDir: flutterFrameworkDir(buildInfo.mode),
Mikkel Nygaard Ravn22832d32018-08-30 16:18:44 +0200353 isSwift: project.ios.isSwift,
Chris Brackencdbdafa2018-05-03 19:40:16 -0700354 dependenciesChanged: !await fingerprinter.doesFingerprintMatch()
xster4d2c2aa2017-12-27 07:30:31 -0800355 );
Chris Brackencdbdafa2018-05-03 19:40:16 -0700356 if (didPodInstall)
357 await fingerprinter.writeFingerprint();
xster4d2c2aa2017-12-27 07:30:31 -0800358 }
359
xsterd401bd72018-02-13 01:56:13 -0800360 final List<String> buildCommands = <String>[
Chinmay Garde66e10062016-03-25 15:36:45 -0700361 '/usr/bin/env',
362 'xcrun',
363 'xcodebuild',
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200364 '-configuration', configuration,
Devon Carew67046f92016-02-20 22:00:11 -0800365 ];
366
xsterd401bd72018-02-13 01:56:13 -0800367 if (logger.isVerbose) {
368 // An environment variable to be passed to xcode_backend.sh determining
369 // whether to echo back executed commands.
370 buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES');
371 } else {
372 // This will print warnings and errors only.
373 buildCommands.add('-quiet');
374 }
375
xster45653952018-03-28 18:58:06 -0700376 if (autoSigningConfigs != null) {
377 for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
378 buildCommands.add('${signingConfig.key}=${signingConfig.value}');
379 }
xsterd401bd72018-02-13 01:56:13 -0800380 buildCommands.add('-allowProvisioningUpdates');
381 buildCommands.add('-allowProvisioningDeviceRegistration');
xster6d2dc052018-02-12 16:44:21 -0800382 }
xsterb232a842017-05-15 12:54:32 -0700383
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200384 final List<FileSystemEntity> contents = app.project.hostAppRoot.listSync();
Collin Jackson6073a7e2016-07-12 17:30:32 -0700385 for (FileSystemEntity entity in contents) {
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800386 if (fs.path.extension(entity.path) == '.xcworkspace') {
xsterd401bd72018-02-13 01:56:13 -0800387 buildCommands.addAll(<String>[
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800388 '-workspace', fs.path.basename(entity.path),
Mikkel Nygaard Ravn32ab3db2017-08-23 11:58:21 +0200389 '-scheme', scheme,
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200390 'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}',
Collin Jackson6073a7e2016-07-12 17:30:32 -0700391 ]);
392 break;
393 }
394 }
395
Devon Carew67046f92016-02-20 22:00:11 -0800396 if (buildForDevice) {
Chris Bracken849676f2018-05-06 18:43:07 -0700397 buildCommands.addAll(<String>['-sdk', 'iphoneos']);
Devon Carew67046f92016-02-20 22:00:11 -0800398 } else {
xsterd401bd72018-02-13 01:56:13 -0800399 buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
Devon Carew67046f92016-02-20 22:00:11 -0800400 }
401
Todd Volkert74e31672016-11-10 10:30:17 -0800402 if (!codesign) {
xsterd401bd72018-02-13 01:56:13 -0800403 buildCommands.addAll(
Todd Volkert74e31672016-11-10 10:30:17 -0800404 <String>[
405 'CODE_SIGNING_ALLOWED=NO',
406 'CODE_SIGNING_REQUIRED=NO',
407 'CODE_SIGNING_IDENTITY=""'
408 ]
409 );
410 }
411
xster1e397d32018-02-15 15:16:23 -0800412 Status buildSubStatus;
413 Status initialBuildStatus;
Ian Hickson3dec6a62018-08-17 13:17:23 -0700414 Directory tempDir;
xster1e397d32018-02-15 15:16:23 -0800415
Greg Spencerefcd9a82018-09-20 15:45:48 -0700416 if (logger.hasTerminal) {
Ian Hickson3dec6a62018-08-17 13:17:23 -0700417 tempDir = fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.');
418 final File scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout');
xster1e397d32018-02-15 15:16:23 -0800419 os.makePipe(scriptOutputPipeFile.path);
420
421 Future<void> listenToScriptOutputLine() async {
422 final List<String> lines = await scriptOutputPipeFile.readAsLines();
423 for (String line in lines) {
424 if (line == 'done') {
425 buildSubStatus?.stop();
426 buildSubStatus = null;
427 } else {
428 initialBuildStatus.cancel();
429 buildSubStatus = logger.startProgress(
430 line,
431 expectSlowOperation: true,
jcollins-g614df692018-02-28 12:09:52 -0800432 progressIndicatorPadding: kDefaultStatusPadding - 7,
xster1e397d32018-02-15 15:16:23 -0800433 );
434 }
435 }
436 return listenToScriptOutputLine();
437 }
438
439 // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
440 listenToScriptOutputLine(); // ignore: unawaited_futures
441
442 buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}');
443 }
444
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200445 final Stopwatch buildStopwatch = Stopwatch()..start();
xster1e397d32018-02-15 15:16:23 -0800446 initialBuildStatus = logger.startProgress('Starting Xcode build...');
xsterd401bd72018-02-13 01:56:13 -0800447 final RunResult buildResult = await runAsync(
448 buildCommands,
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200449 workingDirectory: app.project.hostAppRoot.path,
Ian Hickson0d21e692016-06-14 18:16:55 -0700450 allowReentrantFlutter: true
451 );
xster1e397d32018-02-15 15:16:23 -0800452 buildSubStatus?.stop();
453 initialBuildStatus?.cancel();
454 buildStopwatch.stop();
455 // Free pipe file.
Ian Hickson3dec6a62018-08-17 13:17:23 -0700456 tempDir?.deleteSync(recursive: true);
xster1e397d32018-02-15 15:16:23 -0800457 printStatus(
Greg Spencer7caa6592018-09-19 15:22:43 -0700458 'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
xster1e397d32018-02-15 15:16:23 -0800459 + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}',
460 );
xsterd401bd72018-02-13 01:56:13 -0800461
462 // Run -showBuildSettings again but with the exact same parameters as the build.
463 final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200464 (List<String>
xster80c10bc2018-02-13 16:49:01 -0800465 .from(buildCommands)
466 ..add('-showBuildSettings'))
467 // Undocumented behaviour: xcodebuild craps out if -showBuildSettings
468 // is used together with -allowProvisioningUpdates or
469 // -allowProvisioningDeviceRegistration and freezes forever.
470 .where((String buildCommand) {
471 return !const <String>[
472 '-allowProvisioningUpdates',
473 '-allowProvisioningDeviceRegistration',
474 ].contains(buildCommand);
Devon Carew9d9836f2018-07-09 12:22:46 -0700475 }).toList(),
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200476 workingDirectory: app.project.hostAppRoot.path,
xsterd401bd72018-02-13 01:56:13 -0800477 ));
478
479 if (buildResult.exitCode != 0) {
xster1cc78142017-02-08 18:27:47 -0800480 printStatus('Failed to build iOS app');
xsterd401bd72018-02-13 01:56:13 -0800481 if (buildResult.stderr.isNotEmpty) {
xster1cc78142017-02-08 18:27:47 -0800482 printStatus('Error output from Xcode build:\n↳');
xsterd401bd72018-02-13 01:56:13 -0800483 printStatus(buildResult.stderr, indent: 4);
xster1cc78142017-02-08 18:27:47 -0800484 }
xsterd401bd72018-02-13 01:56:13 -0800485 if (buildResult.stdout.isNotEmpty) {
xster1cc78142017-02-08 18:27:47 -0800486 printStatus('Xcode\'s output:\n↳');
xsterd401bd72018-02-13 01:56:13 -0800487 printStatus(buildResult.stdout, indent: 4);
xster1cc78142017-02-08 18:27:47 -0800488 }
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200489 return XcodeBuildResult(
xster0c8e1812017-02-07 16:59:58 -0800490 success: false,
xsterd401bd72018-02-13 01:56:13 -0800491 stdout: buildResult.stdout,
492 stderr: buildResult.stderr,
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200493 xcodeBuildExecution: XcodeBuildExecution(
xsterd401bd72018-02-13 01:56:13 -0800494 buildCommands: buildCommands,
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200495 appDirectory: app.project.hostAppRoot.path,
xster0c8e1812017-02-07 16:59:58 -0800496 buildForPhysicalDevice: buildForDevice,
xsterd401bd72018-02-13 01:56:13 -0800497 buildSettings: buildSettings,
xster0c8e1812017-02-07 16:59:58 -0800498 ),
499 );
Devon Carew7c478372016-05-29 15:07:41 -0700500 } else {
xsterd401bd72018-02-13 01:56:13 -0800501 final String expectedOutputDirectory = fs.path.join(
502 buildSettings['TARGET_BUILD_DIR'],
503 buildSettings['WRAPPER_NAME'],
504 );
505
Devon Carew7c478372016-05-29 15:07:41 -0700506 String outputDir;
xsterd401bd72018-02-13 01:56:13 -0800507 if (fs.isDirectorySync(expectedOutputDirectory)) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200508 // Copy app folder to a place where other tools can find it without knowing
509 // the BuildInfo.
xsterd401bd72018-02-13 01:56:13 -0800510 outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
Alexander Apreleva92110a2018-01-20 11:11:44 -0800511 if (fs.isDirectorySync(outputDir)) {
512 // Previous output directory might have incompatible artifacts
Chris Bracken041ff622018-09-04 08:50:05 -0700513 // (for example, kernel binary files produced from previous run).
Alexander Apreleva92110a2018-01-20 11:11:44 -0800514 fs.directory(outputDir).deleteSync(recursive: true);
515 }
xsterd401bd72018-02-13 01:56:13 -0800516 copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir));
517 } else {
518 printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200519 }
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200520 return XcodeBuildResult(success: true, output: outputDir);
Dan Rubel573eaf02016-09-16 17:59:43 -0400521 }
522}
523
xster4d2c2aa2017-12-27 07:30:31 -0800524String readGeneratedXcconfig(String appPath) {
525 final String generatedXcconfigPath =
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +0100526 fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig');
xster4d2c2aa2017-12-27 07:30:31 -0800527 final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
528 if (!generatedXcconfigFile.existsSync())
529 return null;
530 return generatedXcconfigFile.readAsStringSync();
531}
532
xsterd401bd72018-02-13 01:56:13 -0800533Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
xsterc2b0a302017-06-07 15:56:13 -0700534 if (result.xcodeBuildExecution != null &&
535 result.xcodeBuildExecution.buildForPhysicalDevice &&
xsterd5d2cdf2017-11-16 22:53:22 -0800536 result.stdout?.contains('BCEROR') == true &&
537 // May need updating if Xcode changes its outputs.
538 result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) {
xsterc2b0a302017-06-07 15:56:13 -0700539 printError(noProvisioningProfileInstruction, emphasis: true);
540 return;
541 }
Chris Bracken024cebf2018-02-05 18:11:07 -0800542 // Make sure the user has specified one of:
543 // * DEVELOPMENT_TEAM (automatic signing)
544 // * PROVISIONING_PROFILE (manual signing)
xsterc2b0a302017-06-07 15:56:13 -0700545 if (result.xcodeBuildExecution != null &&
546 result.xcodeBuildExecution.buildForPhysicalDevice &&
xsterd401bd72018-02-13 01:56:13 -0800547 !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
548 result.xcodeBuildExecution.buildSettings.containsKey)
549 ) {
xsterc2b0a302017-06-07 15:56:13 -0700550 printError(noDevelopmentTeamInstruction, emphasis: true);
551 return;
552 }
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200553 if (result.xcodeBuildExecution != null &&
554 result.xcodeBuildExecution.buildForPhysicalDevice &&
Yegor85473d02018-04-19 18:29:49 -0700555 result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) {
xsterc2b0a302017-06-07 15:56:13 -0700556 printError('');
557 printError('It appears that your application still contains the default signing identifier.');
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +0100558 printError("Try replacing 'com.example' with your signing id in Xcode:");
xsterc2b0a302017-06-07 15:56:13 -0700559 printError(' open ios/Runner.xcworkspace');
560 return;
Dan Rubel573eaf02016-09-16 17:59:43 -0400561 }
562 if (result.stdout?.contains('Code Sign error') == true) {
563 printError('');
564 printError('It appears that there was a problem signing your application prior to installation on the device.');
565 printError('');
xsterc2b0a302017-06-07 15:56:13 -0700566 printError('Verify that the Bundle Identifier in your project is your signing id in Xcode');
567 printError(' open ios/Runner.xcworkspace');
568 printError('');
569 printError("Also try selecting 'Product > Build' to fix the problem:");
Dan Rubel573eaf02016-09-16 17:59:43 -0400570 return;
Devon Carew67046f92016-02-20 22:00:11 -0800571 }
Devon Carew7c478372016-05-29 15:07:41 -0700572}
Devon Carewf68d86d2016-03-03 15:10:45 -0800573
Devon Carew7c478372016-05-29 15:07:41 -0700574class XcodeBuildResult {
xster0c8e1812017-02-07 16:59:58 -0800575 XcodeBuildResult(
576 {
577 @required this.success,
578 this.output,
579 this.stdout,
580 this.stderr,
581 this.xcodeBuildExecution,
582 }
583 );
Devon Carew7c478372016-05-29 15:07:41 -0700584
585 final bool success;
586 final String output;
Dan Rubel573eaf02016-09-16 17:59:43 -0400587 final String stdout;
588 final String stderr;
xster0c8e1812017-02-07 16:59:58 -0800589 /// The invocation of the build that resulted in this result instance.
590 final XcodeBuildExecution xcodeBuildExecution;
591}
592
593/// Describes an invocation of a Xcode build command.
594class XcodeBuildExecution {
595 XcodeBuildExecution(
xster0c8e1812017-02-07 16:59:58 -0800596 {
xsterd401bd72018-02-13 01:56:13 -0800597 @required this.buildCommands,
598 @required this.appDirectory,
xster0c8e1812017-02-07 16:59:58 -0800599 @required this.buildForPhysicalDevice,
xsterd401bd72018-02-13 01:56:13 -0800600 @required this.buildSettings,
xster0c8e1812017-02-07 16:59:58 -0800601 }
602 );
603
604 /// The original list of Xcode build commands used to produce this build result.
605 final List<String> buildCommands;
606 final String appDirectory;
607 final bool buildForPhysicalDevice;
xsterd401bd72018-02-13 01:56:13 -0800608 /// The build settings corresponding to the [buildCommands] invocation.
609 final Map<String, String> buildSettings;
Devon Carew67046f92016-02-20 22:00:11 -0800610}
611
Alexandre Ardhuin7667db62018-03-14 06:24:49 +0100612const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
Devon Carew67046f92016-02-20 22:00:11 -0800613
614bool _checkXcodeVersion() {
Todd Volkert417c2f22017-01-25 16:06:41 -0800615 if (!platform.isMacOS)
Devon Carew67046f92016-02-20 22:00:11 -0800616 return false;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100617 if (!xcodeProjectInterpreter.isInstalled) {
Jakob Andersen60c0c3d2017-03-10 13:43:57 +0100618 printError('Cannot find "xcodebuild". $_xcodeRequirement');
Devon Carew67046f92016-02-20 22:00:11 -0800619 return false;
620 }
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100621 if (!xcode.isVersionSatisfactory) {
622 printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
623 return false;
624 }
Devon Carew67046f92016-02-20 22:00:11 -0800625 return true;
626}
627
Ian Hicksond745e202016-03-12 00:32:34 -0800628Future<Null> _addServicesToBundle(Directory bundle) async {
Chris Bracken7a093162017-03-03 17:50:46 -0800629 final List<Map<String, String>> services = <Map<String, String>>[];
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200630 printTrace('Trying to resolve native pub services.');
Devon Carew67046f92016-02-20 22:00:11 -0800631
632 // Step 1: Parse the service configuration yaml files present in the service
633 // pub packages.
634 await parseServiceConfigs(services);
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200635 printTrace('Found ${services.length} service definition(s).');
Devon Carew67046f92016-02-20 22:00:11 -0800636
637 // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200638 final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks'));
Devon Carew67046f92016-02-20 22:00:11 -0800639 await _copyServiceFrameworks(services, frameworksDirectory);
640
641 // Step 3: Copy the service definitions manifest at the correct spot for
642 // xcodebuild to pick up.
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200643 final File manifestFile = fs.file(fs.path.join(bundle.path, 'ServiceDefinitions.json'));
Devon Carew67046f92016-02-20 22:00:11 -0800644 _copyServiceDefinitionsManifest(services, manifestFile);
645}
646
Ian Hicksond745e202016-03-12 00:32:34 -0800647Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800648 printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'.");
Devon Carew67046f92016-02-20 22:00:11 -0800649 frameworksDirectory.createSync(recursive: true);
650 for (Map<String, String> service in services) {
Chris Bracken7a093162017-03-03 17:50:46 -0800651 final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
652 final File dylib = fs.file(dylibPath);
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200653 printTrace('Copying ${dylib.path} into bundle.');
Devon Carew67046f92016-02-20 22:00:11 -0800654 if (!dylib.existsSync()) {
655 printError("The service dylib '${dylib.path}' does not exist.");
656 continue;
657 }
658 // Shell out so permissions on the dylib are preserved.
Todd Volkert60c5ffc2017-04-25 17:23:00 -0700659 await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
Devon Carew67046f92016-02-20 22:00:11 -0800660 }
661}
662
663void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
664 printTrace("Creating service definitions manifest at '${manifest.path}'");
Chris Bracken7a093162017-03-03 17:50:46 -0800665 final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{
Devon Carew67046f92016-02-20 22:00:11 -0800666 'name': service['name'],
667 // Since we have already moved it to the Frameworks directory. Strip away
668 // the directory and basenames.
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800669 'framework': fs.path.basenameWithoutExtension(service['ios-framework'])
Devon Carew67046f92016-02-20 22:00:11 -0800670 }).toList();
Jason Simmons466d1542018-03-12 11:06:32 -0700671 final Map<String, dynamic> jsonObject = <String, dynamic>{ 'services' : jsonServices };
Leaf Petersen32f94442018-07-20 15:07:24 -0700672 manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true);
Devon Carew67046f92016-02-20 22:00:11 -0800673}
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100674
Mikkel Nygaard Ravn22832d32018-08-30 16:18:44 +0200675Future<bool> upgradePbxProjWithFlutterAssets(IosProject project) async {
676 final File xcodeProjectFile = project.xcodeProjectInfoFile;
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100677 assert(await xcodeProjectFile.exists());
678 final List<String> lines = await xcodeProjectFile.readAsLines();
679
Mikkel Nygaard Ravna600fe72018-09-25 21:21:13 +0200680 if (lines.any((String line) => line.contains('flutter_assets in Resources')))
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100681 return true;
682
Alexandre Ardhuin841d5d72018-02-01 07:51:26 +0100683 const String l1 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };';
684 const String l2 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };';
685 const String l3 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };';
686 const String l4 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };';
687 const String l5 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,';
688 const String l6 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,';
689 const String l7 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,';
690 const String l8 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,';
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100691
692
Mikkel Nygaard Ravn22832d32018-08-30 16:18:44 +0200693 printStatus("Upgrading project.pbxproj of ${project.hostAppBundleName}' to include the "
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100694 "'flutter_assets' directory");
695
696 if (!lines.contains(l1) || !lines.contains(l3) ||
697 !lines.contains(l5) || !lines.contains(l7)) {
698 printError('Automatic upgrade of project.pbxproj failed.');
Chris Brackencdbdafa2018-05-03 19:40:16 -0700699 printError(' To manually upgrade, open ${xcodeProjectFile.path}:');
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100700 printError(' Add the following line in the "PBXBuildFile" section');
701 printError(l2);
702 printError(' Add the following line in the "PBXFileReference" section');
703 printError(l4);
704 printError(' Add the following line in the "children" list of the "Flutter" group in the "PBXGroup" section');
705 printError(l6);
706 printError(' Add the following line in the "files" list of "Resources" in the "PBXResourcesBuildPhase" section');
707 printError(l8);
708 return false;
709 }
710
711 lines.insert(lines.indexOf(l1) + 1, l2);
712 lines.insert(lines.indexOf(l3) + 1, l4);
713 lines.insert(lines.indexOf(l5) + 1, l6);
714 lines.insert(lines.indexOf(l7) + 1, l8);
715
Alexandre Ardhuin841d5d72018-02-01 07:51:26 +0100716 const String l9 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB71CF902C7004384FC /* app.flx */; };';
717 const String l10 = ' 9740EEB71CF902C7004384FC /* app.flx */ = {isa = PBXFileReference; lastKnownFileType = file; name = app.flx; path = Flutter/app.flx; sourceTree = "<group>"; };';
718 const String l11 = ' 9740EEB71CF902C7004384FC /* app.flx */,';
719 const String l12 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */,';
Sarah Zakarias73552ec2017-12-18 18:58:44 +0100720
721 if (lines.contains(l9)) {
722 printStatus('Removing app.flx from project.pbxproj since it has been '
723 'replaced with flutter_assets.');
724 lines.remove(l9);
725 lines.remove(l10);
726 lines.remove(l11);
727 lines.remove(l12);
728 }
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100729
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200730 final StringBuffer buffer = StringBuffer();
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100731 lines.forEach(buffer.writeln);
732 await xcodeProjectFile.writeAsString(buffer.toString());
733 return true;
Chris Bracken7fb78522017-12-15 18:13:10 -0800734}