blob: 85d4d87a43a4072a4e06bf0b9859187da200b4b0 [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';
Sigurd Meldgaard6a8f9042018-07-16 16:21:20 +020023import '../flutter_manifest.dart';
Devon Carew67046f92016-02-20 22:00:11 -080024import '../globals.dart';
Jakob Andersenb61e1692017-03-23 14:59:12 +010025import '../plugins.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 Brackend6ec71d2017-06-15 19:03:24 -070035
Todd Volkert8d11f5c2018-03-28 10:58:28 -070036Xcode get xcode => context[Xcode];
Chris Brackenc04f7122017-06-15 19:24:07 -070037
Chris Brackend6ec71d2017-06-15 19:03:24 -070038class IMobileDevice {
39 const IMobileDevice();
40
41 bool get isInstalled => exitsHappy(<String>['idevice_id', '-h']);
42
43 /// Returns true if libimobiledevice is installed and working as expected.
44 ///
45 /// Older releases of libimobiledevice fail to work with iOS 10.3 and above.
46 Future<bool> get isWorking async {
47 if (!isInstalled)
48 return false;
49
50 // If no device is attached, we're unable to detect any problems. Assume all is well.
51 final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult;
52 if (result.exitCode != 0 || result.stdout.isEmpty)
53 return true;
54
55 // Check that we can look up the names of any attached devices.
56 return await exitsHappyAsync(<String>['idevicename']);
57 }
58
Chris Brackeneba6ceb2017-09-01 10:10:49 -070059 Future<String> getAvailableDeviceIDs() async {
60 try {
61 final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']);
62 if (result.exitCode != 0)
63 throw new ToolExit('idevice_id returned an error:\n${result.stderr}');
64 return result.stdout;
65 } on ProcessException {
66 throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
67 }
68 }
69
70 Future<String> getInfoForDevice(String deviceID, String key) async {
71 try {
Mihail Slavchevaf0afff2017-10-19 01:25:49 +030072 final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key, '--simple']);
Chris Brackeneba6ceb2017-09-01 10:10:49 -070073 if (result.exitCode != 0)
74 throw new ToolExit('idevice_id returned an error:\n${result.stderr}');
75 return result.stdout.trim();
76 } on ProcessException {
77 throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
78 }
79 }
80
Chris Bracken66502132017-06-16 14:33:49 -070081 /// Starts `idevicesyslog` and returns the running process.
82 Future<Process> startLogger() => runCommand(<String>['idevicesyslog']);
83
Greg Spencer0259be92017-11-17 10:05:21 -080084 /// Captures a screenshot to the specified outputFile.
Keerti Parthasarathyaecb7d92018-06-12 09:30:10 -070085 Future<void> takeScreenshot(File outputFile) {
Chris Bracken66502132017-06-16 14:33:49 -070086 return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]);
87 }
Chris Brackend6ec71d2017-06-15 19:03:24 -070088}
89
Chris Bracken5d2a4602017-02-01 16:16:33 -080090class Xcode {
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +010091 bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
Chinmay Gardec5056b92016-02-19 13:19:11 -080092
Devon Carewc3eec6e2016-03-24 13:36:32 -070093 String _xcodeSelectPath;
Chris Bracken845c1b72017-06-23 16:58:14 -070094 String get xcodeSelectPath {
95 if (_xcodeSelectPath == null) {
96 try {
Chris Bracken2ebb9e52017-06-23 18:50:27 -070097 _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim();
Chris Bracken845c1b72017-06-23 16:58:14 -070098 } on ProcessException {
99 // Ignore: return null below.
100 }
101 }
102 return _xcodeSelectPath;
103 }
Devon Carewc3eec6e2016-03-24 13:36:32 -0700104
Chris Bracken845c1b72017-06-23 16:58:14 -0700105 bool get isInstalled {
106 if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
107 return false;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100108 return xcodeProjectInterpreter.isInstalled;
Chris Bracken845c1b72017-06-23 16:58:14 -0700109 }
Chinmay Gardec5056b92016-02-19 13:19:11 -0800110
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100111 int get majorVersion => xcodeProjectInterpreter.majorVersion;
112
113 int get minorVersion => xcodeProjectInterpreter.minorVersion;
114
115 String get versionText => xcodeProjectInterpreter.versionText;
116
Devon Carewc3eec6e2016-03-24 13:36:32 -0700117 bool _eulaSigned;
Devon Carew16f9e382016-02-21 00:41:14 -0800118 /// Has the EULA been signed?
Chris Bracken845c1b72017-06-23 16:58:14 -0700119 bool get eulaSigned {
120 if (_eulaSigned == null) {
121 try {
122 final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'clang']);
123 if (result.stdout != null && result.stdout.contains('license'))
124 _eulaSigned = false;
125 else if (result.stderr != null && result.stderr.contains('license'))
126 _eulaSigned = false;
127 else
128 _eulaSigned = true;
129 } on ProcessException {
130 _eulaSigned = false;
131 }
132 }
133 return _eulaSigned;
134 }
Devon Carew16f9e382016-02-21 00:41:14 -0800135
Jonah Williams298f4ef2018-03-17 10:57:51 -0700136 bool _isSimctlInstalled;
137
138 /// Verifies that simctl is installed by trying to run it.
139 bool get isSimctlInstalled {
140 if (_isSimctlInstalled == null) {
141 try {
142 // This command will error if additional components need to be installed in
143 // xcode 9.2 and above.
144 final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']);
Jonah Williams91dcfc52018-03-19 10:53:04 -0700145 _isSimctlInstalled = result.stderr == null || result.stderr == '';
Jonah Williams298f4ef2018-03-17 10:57:51 -0700146 } on ProcessException {
147 _isSimctlInstalled = false;
148 }
149 }
150 return _isSimctlInstalled;
151 }
152
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100153 bool get isVersionSatisfactory {
154 if (!xcodeProjectInterpreter.isInstalled)
Devon Carew4ac18682016-03-28 16:20:43 -0700155 return false;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100156 if (majorVersion > kXcodeRequiredVersionMajor)
157 return true;
158 if (majorVersion == kXcodeRequiredVersionMajor)
159 return minorVersion >= kXcodeRequiredVersionMinor;
160 return false;
Chinmay Gardec5056b92016-02-19 13:19:11 -0800161 }
Chris Bracken26895602018-04-25 20:50:16 -0700162
163 Future<RunResult> cc(List<String> args) {
164 return runCheckedAsync(<String>['xcrun', 'cc']..addAll(args));
165 }
166
167 Future<RunResult> clang(List<String> args) {
168 return runCheckedAsync(<String>['xcrun', 'clang']..addAll(args));
169 }
Danny Tuppenyd3f61282018-07-19 10:32:44 +0100170
171 String getSimulatorPath() {
172 if (xcodeSelectPath == null)
173 return null;
174 final List<String> searchPaths = <String>[
175 fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'),
176 ];
177 return searchPaths.where((String p) => p != null).firstWhere(
178 (String p) => fs.directory(p).existsSync(),
179 orElse: () => null,
180 );
181 }
Devon Carew7ae6f7f2016-02-16 18:23:43 -0800182}
Devon Carew67046f92016-02-20 22:00:11 -0800183
Adam Barth612a0972016-06-02 08:57:13 -0700184Future<XcodeBuildResult> buildXcodeProject({
Todd Volkert904d5242016-10-13 16:17:50 -0700185 BuildableIOSApp app,
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200186 BuildInfo buildInfo,
Simon Lightfootbe6501a2018-05-21 11:54:38 +0100187 String targetOverride,
Adam Barth612a0972016-06-02 08:57:13 -0700188 bool buildForDevice,
Alexandre Ardhuin09276be2018-06-05 08:50:40 +0200189 bool codesign = true,
190 bool usesTerminalUi = true,
Adam Barth612a0972016-06-02 08:57:13 -0700191}) async {
Chris Brackencdbdafa2018-05-03 19:40:16 -0700192 if (!await upgradePbxProjWithFlutterAssets(app.name, app.appDirectory))
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100193 return new XcodeBuildResult(success: false);
194
Devon Carew67046f92016-02-20 22:00:11 -0800195 if (!_checkXcodeVersion())
xster0c8e1812017-02-07 16:59:58 -0800196 return new XcodeBuildResult(success: false);
Devon Carew67046f92016-02-20 22:00:11 -0800197
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +0100198 final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.appDirectory);
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200199 if (!projectInfo.targets.contains('Runner')) {
200 printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
201 printError('Open Xcode to fix the problem:');
202 printError(' open ios/Runner.xcworkspace');
203 return new XcodeBuildResult(success: false);
204 }
205 final String scheme = projectInfo.schemeFor(buildInfo);
206 if (scheme == null) {
207 printError('');
208 if (projectInfo.definesCustomSchemes) {
209 printError('The Xcode project defines schemes: ${projectInfo.schemes.join(', ')}');
210 printError('You must specify a --flavor option to select one of them.');
211 } else {
212 printError('The Xcode project does not define custom schemes.');
213 printError('You cannot use the --flavor option.');
214 }
215 return new XcodeBuildResult(success: false);
216 }
217 final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme);
218 if (configuration == null) {
219 printError('');
220 printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}');
221 printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.');
222 printError('Open Xcode to fix the problem:');
223 printError(' open ios/Runner.xcworkspace');
224 return new XcodeBuildResult(success: false);
225 }
226
xster45653952018-03-28 18:58:06 -0700227 Map<String, String> autoSigningConfigs;
xsterc2b0a302017-06-07 15:56:13 -0700228 if (codesign && buildForDevice)
xster45653952018-03-28 18:58:06 -0700229 autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi);
xsterb232a842017-05-15 12:54:32 -0700230
Devon Carew67046f92016-02-20 22:00:11 -0800231 // Before the build, all service definitions must be updated and the dylibs
232 // copied over to a location that is suitable for Xcodebuild to find them.
Jakob Andersen60c0c3d2017-03-10 13:43:57 +0100233 final Directory appDirectory = fs.directory(app.appDirectory);
234 await _addServicesToBundle(appDirectory);
Devon Carew67046f92016-02-20 22:00:11 -0800235
Sigurd Meldgaard6a8f9042018-07-16 16:21:20 +0200236 final FlutterManifest manifest = await FlutterManifest.createFromPath(
237 fs.currentDirectory.childFile('pubspec.yaml').path,
238 );
239 updateGeneratedXcodeProperties(
240 projectPath: fs.currentDirectory.path,
241 buildInfo: buildInfo,
Simon Lightfootbe6501a2018-05-21 11:54:38 +0100242 targetOverride: targetOverride,
Chris Bracken2ae48842018-06-13 12:46:39 -0700243 previewDart2: buildInfo.previewDart2,
Sigurd Meldgaard6a8f9042018-07-16 16:21:20 +0200244 manifest: manifest,
xster837f1012017-05-04 12:26:58 -0700245 );
246
Sigurd Meldgaard6a8f9042018-07-16 16:21:20 +0200247 if (hasPlugins()) {
Chris Brackencdbdafa2018-05-03 19:40:16 -0700248 final String iosPath = fs.path.join(fs.currentDirectory.path, app.appDirectory);
249 // If the Xcode project, Podfile, or Generated.xcconfig have changed since
250 // last run, pods should be updated.
251 final Fingerprinter fingerprinter = new Fingerprinter(
252 fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'),
253 paths: <String>[
254 _getPbxProjPath(app.appDirectory),
255 fs.path.join(iosPath, 'Podfile'),
256 fs.path.join(iosPath, 'Flutter', 'Generated.xcconfig'),
257 ],
258 properties: <String, String>{},
259 );
260 final bool didPodInstall = await cocoaPods.processPods(
Sigurd Meldgaard6a8f9042018-07-16 16:21:20 +0200261 appIosDirectory: appDirectory,
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +0100262 iosEngineDir: flutterFrameworkDir(buildInfo.mode),
263 isSwift: app.isSwift,
Chris Brackencdbdafa2018-05-03 19:40:16 -0700264 dependenciesChanged: !await fingerprinter.doesFingerprintMatch()
xster4d2c2aa2017-12-27 07:30:31 -0800265 );
Chris Brackencdbdafa2018-05-03 19:40:16 -0700266 if (didPodInstall)
267 await fingerprinter.writeFingerprint();
xster4d2c2aa2017-12-27 07:30:31 -0800268 }
269
xsterd401bd72018-02-13 01:56:13 -0800270 final List<String> buildCommands = <String>[
Chinmay Garde66e10062016-03-25 15:36:45 -0700271 '/usr/bin/env',
272 'xcrun',
273 'xcodebuild',
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200274 '-configuration', configuration,
Devon Carew67046f92016-02-20 22:00:11 -0800275 ];
276
xsterd401bd72018-02-13 01:56:13 -0800277 if (logger.isVerbose) {
278 // An environment variable to be passed to xcode_backend.sh determining
279 // whether to echo back executed commands.
280 buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES');
281 } else {
282 // This will print warnings and errors only.
283 buildCommands.add('-quiet');
284 }
285
xster45653952018-03-28 18:58:06 -0700286 if (autoSigningConfigs != null) {
287 for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
288 buildCommands.add('${signingConfig.key}=${signingConfig.value}');
289 }
xsterd401bd72018-02-13 01:56:13 -0800290 buildCommands.add('-allowProvisioningUpdates');
291 buildCommands.add('-allowProvisioningDeviceRegistration');
xster6d2dc052018-02-12 16:44:21 -0800292 }
xsterb232a842017-05-15 12:54:32 -0700293
Chris Bracken7a093162017-03-03 17:50:46 -0800294 final List<FileSystemEntity> contents = fs.directory(app.appDirectory).listSync();
Collin Jackson6073a7e2016-07-12 17:30:32 -0700295 for (FileSystemEntity entity in contents) {
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800296 if (fs.path.extension(entity.path) == '.xcworkspace') {
xsterd401bd72018-02-13 01:56:13 -0800297 buildCommands.addAll(<String>[
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800298 '-workspace', fs.path.basename(entity.path),
Mikkel Nygaard Ravn32ab3db2017-08-23 11:58:21 +0200299 '-scheme', scheme,
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200300 'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}',
Collin Jackson6073a7e2016-07-12 17:30:32 -0700301 ]);
302 break;
303 }
304 }
305
Devon Carew67046f92016-02-20 22:00:11 -0800306 if (buildForDevice) {
Chris Bracken849676f2018-05-06 18:43:07 -0700307 buildCommands.addAll(<String>['-sdk', 'iphoneos']);
Devon Carew67046f92016-02-20 22:00:11 -0800308 } else {
xsterd401bd72018-02-13 01:56:13 -0800309 buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
Devon Carew67046f92016-02-20 22:00:11 -0800310 }
311
Todd Volkert74e31672016-11-10 10:30:17 -0800312 if (!codesign) {
xsterd401bd72018-02-13 01:56:13 -0800313 buildCommands.addAll(
Todd Volkert74e31672016-11-10 10:30:17 -0800314 <String>[
315 'CODE_SIGNING_ALLOWED=NO',
316 'CODE_SIGNING_REQUIRED=NO',
317 'CODE_SIGNING_IDENTITY=""'
318 ]
319 );
320 }
321
xster1e397d32018-02-15 15:16:23 -0800322 Status buildSubStatus;
323 Status initialBuildStatus;
324 Directory scriptOutputPipeTempDirectory;
325
326 if (logger.supportsColor) {
327 scriptOutputPipeTempDirectory = fs.systemTempDirectory
328 .createTempSync('flutter_build_log_pipe');
329 final File scriptOutputPipeFile =
330 scriptOutputPipeTempDirectory.childFile('pipe_to_stdout');
331 os.makePipe(scriptOutputPipeFile.path);
332
333 Future<void> listenToScriptOutputLine() async {
334 final List<String> lines = await scriptOutputPipeFile.readAsLines();
335 for (String line in lines) {
336 if (line == 'done') {
337 buildSubStatus?.stop();
338 buildSubStatus = null;
339 } else {
340 initialBuildStatus.cancel();
341 buildSubStatus = logger.startProgress(
342 line,
343 expectSlowOperation: true,
jcollins-g614df692018-02-28 12:09:52 -0800344 progressIndicatorPadding: kDefaultStatusPadding - 7,
xster1e397d32018-02-15 15:16:23 -0800345 );
346 }
347 }
348 return listenToScriptOutputLine();
349 }
350
351 // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
352 listenToScriptOutputLine(); // ignore: unawaited_futures
353
354 buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}');
355 }
356
357 final Stopwatch buildStopwatch = new Stopwatch()..start();
358 initialBuildStatus = logger.startProgress('Starting Xcode build...');
xsterd401bd72018-02-13 01:56:13 -0800359 final RunResult buildResult = await runAsync(
360 buildCommands,
Devon Carew14483582016-08-09 14:38:13 -0700361 workingDirectory: app.appDirectory,
Ian Hickson0d21e692016-06-14 18:16:55 -0700362 allowReentrantFlutter: true
363 );
xster1e397d32018-02-15 15:16:23 -0800364 buildSubStatus?.stop();
365 initialBuildStatus?.cancel();
366 buildStopwatch.stop();
367 // Free pipe file.
368 scriptOutputPipeTempDirectory?.deleteSync(recursive: true);
369 printStatus(
Devon Carew13bafba2018-04-04 07:22:58 -0700370 'Xcode build done.',
371 ansiAlternative: 'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
xster1e397d32018-02-15 15:16:23 -0800372 + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}',
373 );
xsterd401bd72018-02-13 01:56:13 -0800374
375 // Run -showBuildSettings again but with the exact same parameters as the build.
376 final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(
xster80c10bc2018-02-13 16:49:01 -0800377 (new List<String>
378 .from(buildCommands)
379 ..add('-showBuildSettings'))
380 // Undocumented behaviour: xcodebuild craps out if -showBuildSettings
381 // is used together with -allowProvisioningUpdates or
382 // -allowProvisioningDeviceRegistration and freezes forever.
383 .where((String buildCommand) {
384 return !const <String>[
385 '-allowProvisioningUpdates',
386 '-allowProvisioningDeviceRegistration',
387 ].contains(buildCommand);
Devon Carew9d9836f2018-07-09 12:22:46 -0700388 }).toList(),
xsterd401bd72018-02-13 01:56:13 -0800389 workingDirectory: app.appDirectory,
390 ));
391
392 if (buildResult.exitCode != 0) {
xster1cc78142017-02-08 18:27:47 -0800393 printStatus('Failed to build iOS app');
xsterd401bd72018-02-13 01:56:13 -0800394 if (buildResult.stderr.isNotEmpty) {
xster1cc78142017-02-08 18:27:47 -0800395 printStatus('Error output from Xcode build:\n↳');
xsterd401bd72018-02-13 01:56:13 -0800396 printStatus(buildResult.stderr, indent: 4);
xster1cc78142017-02-08 18:27:47 -0800397 }
xsterd401bd72018-02-13 01:56:13 -0800398 if (buildResult.stdout.isNotEmpty) {
xster1cc78142017-02-08 18:27:47 -0800399 printStatus('Xcode\'s output:\n↳');
xsterd401bd72018-02-13 01:56:13 -0800400 printStatus(buildResult.stdout, indent: 4);
xster1cc78142017-02-08 18:27:47 -0800401 }
xster0c8e1812017-02-07 16:59:58 -0800402 return new XcodeBuildResult(
403 success: false,
xsterd401bd72018-02-13 01:56:13 -0800404 stdout: buildResult.stdout,
405 stderr: buildResult.stderr,
xster0c8e1812017-02-07 16:59:58 -0800406 xcodeBuildExecution: new XcodeBuildExecution(
xsterd401bd72018-02-13 01:56:13 -0800407 buildCommands: buildCommands,
408 appDirectory: app.appDirectory,
xster0c8e1812017-02-07 16:59:58 -0800409 buildForPhysicalDevice: buildForDevice,
xsterd401bd72018-02-13 01:56:13 -0800410 buildSettings: buildSettings,
xster0c8e1812017-02-07 16:59:58 -0800411 ),
412 );
Devon Carew7c478372016-05-29 15:07:41 -0700413 } else {
xsterd401bd72018-02-13 01:56:13 -0800414 final String expectedOutputDirectory = fs.path.join(
415 buildSettings['TARGET_BUILD_DIR'],
416 buildSettings['WRAPPER_NAME'],
417 );
418
Devon Carew7c478372016-05-29 15:07:41 -0700419 String outputDir;
xsterd401bd72018-02-13 01:56:13 -0800420 if (fs.isDirectorySync(expectedOutputDirectory)) {
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200421 // Copy app folder to a place where other tools can find it without knowing
422 // the BuildInfo.
xsterd401bd72018-02-13 01:56:13 -0800423 outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
Alexander Apreleva92110a2018-01-20 11:11:44 -0800424 if (fs.isDirectorySync(outputDir)) {
425 // Previous output directory might have incompatible artifacts
Chris Bracken2ae48842018-06-13 12:46:39 -0700426 // (for example, kernel binary files produced from previous `--preview-dart-2` run).
Alexander Apreleva92110a2018-01-20 11:11:44 -0800427 fs.directory(outputDir).deleteSync(recursive: true);
428 }
xsterd401bd72018-02-13 01:56:13 -0800429 copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir));
430 } else {
431 printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200432 }
433 return new XcodeBuildResult(success: true, output: outputDir);
Dan Rubel573eaf02016-09-16 17:59:43 -0400434 }
435}
436
xster4d2c2aa2017-12-27 07:30:31 -0800437String readGeneratedXcconfig(String appPath) {
438 final String generatedXcconfigPath =
Mikkel Nygaard Ravn20004352018-02-16 10:17:28 +0100439 fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig');
xster4d2c2aa2017-12-27 07:30:31 -0800440 final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
441 if (!generatedXcconfigFile.existsSync())
442 return null;
443 return generatedXcconfigFile.readAsStringSync();
444}
445
xsterd401bd72018-02-13 01:56:13 -0800446Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
xsterc2b0a302017-06-07 15:56:13 -0700447 if (result.xcodeBuildExecution != null &&
448 result.xcodeBuildExecution.buildForPhysicalDevice &&
xsterd5d2cdf2017-11-16 22:53:22 -0800449 result.stdout?.contains('BCEROR') == true &&
450 // May need updating if Xcode changes its outputs.
451 result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) {
xsterc2b0a302017-06-07 15:56:13 -0700452 printError(noProvisioningProfileInstruction, emphasis: true);
453 return;
454 }
Chris Bracken024cebf2018-02-05 18:11:07 -0800455 // Make sure the user has specified one of:
456 // * DEVELOPMENT_TEAM (automatic signing)
457 // * PROVISIONING_PROFILE (manual signing)
xsterc2b0a302017-06-07 15:56:13 -0700458 if (result.xcodeBuildExecution != null &&
459 result.xcodeBuildExecution.buildForPhysicalDevice &&
xsterd401bd72018-02-13 01:56:13 -0800460 !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
461 result.xcodeBuildExecution.buildSettings.containsKey)
462 ) {
xsterc2b0a302017-06-07 15:56:13 -0700463 printError(noDevelopmentTeamInstruction, emphasis: true);
464 return;
465 }
Mikkel Nygaard Ravn9496e6d2017-08-23 10:55:35 +0200466 if (result.xcodeBuildExecution != null &&
467 result.xcodeBuildExecution.buildForPhysicalDevice &&
Yegor85473d02018-04-19 18:29:49 -0700468 result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) {
xsterc2b0a302017-06-07 15:56:13 -0700469 printError('');
470 printError('It appears that your application still contains the default signing identifier.');
Mikkel Nygaard Ravn43532972018-01-18 09:21:24 +0100471 printError("Try replacing 'com.example' with your signing id in Xcode:");
xsterc2b0a302017-06-07 15:56:13 -0700472 printError(' open ios/Runner.xcworkspace');
473 return;
Dan Rubel573eaf02016-09-16 17:59:43 -0400474 }
475 if (result.stdout?.contains('Code Sign error') == true) {
476 printError('');
477 printError('It appears that there was a problem signing your application prior to installation on the device.');
478 printError('');
xsterc2b0a302017-06-07 15:56:13 -0700479 printError('Verify that the Bundle Identifier in your project is your signing id in Xcode');
480 printError(' open ios/Runner.xcworkspace');
481 printError('');
482 printError("Also try selecting 'Product > Build' to fix the problem:");
Dan Rubel573eaf02016-09-16 17:59:43 -0400483 return;
Devon Carew67046f92016-02-20 22:00:11 -0800484 }
Devon Carew7c478372016-05-29 15:07:41 -0700485}
Devon Carewf68d86d2016-03-03 15:10:45 -0800486
Devon Carew7c478372016-05-29 15:07:41 -0700487class XcodeBuildResult {
xster0c8e1812017-02-07 16:59:58 -0800488 XcodeBuildResult(
489 {
490 @required this.success,
491 this.output,
492 this.stdout,
493 this.stderr,
494 this.xcodeBuildExecution,
495 }
496 );
Devon Carew7c478372016-05-29 15:07:41 -0700497
498 final bool success;
499 final String output;
Dan Rubel573eaf02016-09-16 17:59:43 -0400500 final String stdout;
501 final String stderr;
xster0c8e1812017-02-07 16:59:58 -0800502 /// The invocation of the build that resulted in this result instance.
503 final XcodeBuildExecution xcodeBuildExecution;
504}
505
506/// Describes an invocation of a Xcode build command.
507class XcodeBuildExecution {
508 XcodeBuildExecution(
xster0c8e1812017-02-07 16:59:58 -0800509 {
xsterd401bd72018-02-13 01:56:13 -0800510 @required this.buildCommands,
511 @required this.appDirectory,
xster0c8e1812017-02-07 16:59:58 -0800512 @required this.buildForPhysicalDevice,
xsterd401bd72018-02-13 01:56:13 -0800513 @required this.buildSettings,
xster0c8e1812017-02-07 16:59:58 -0800514 }
515 );
516
517 /// The original list of Xcode build commands used to produce this build result.
518 final List<String> buildCommands;
519 final String appDirectory;
520 final bool buildForPhysicalDevice;
xsterd401bd72018-02-13 01:56:13 -0800521 /// The build settings corresponding to the [buildCommands] invocation.
522 final Map<String, String> buildSettings;
Devon Carew67046f92016-02-20 22:00:11 -0800523}
524
Alexandre Ardhuin7667db62018-03-14 06:24:49 +0100525const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
Devon Carew67046f92016-02-20 22:00:11 -0800526
527bool _checkXcodeVersion() {
Todd Volkert417c2f22017-01-25 16:06:41 -0800528 if (!platform.isMacOS)
Devon Carew67046f92016-02-20 22:00:11 -0800529 return false;
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100530 if (!xcodeProjectInterpreter.isInstalled) {
Jakob Andersen60c0c3d2017-03-10 13:43:57 +0100531 printError('Cannot find "xcodebuild". $_xcodeRequirement');
Devon Carew67046f92016-02-20 22:00:11 -0800532 return false;
533 }
Mikkel Nygaard Ravn0d596792018-03-07 08:41:23 +0100534 if (!xcode.isVersionSatisfactory) {
535 printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
536 return false;
537 }
Devon Carew67046f92016-02-20 22:00:11 -0800538 return true;
539}
540
Ian Hicksond745e202016-03-12 00:32:34 -0800541Future<Null> _addServicesToBundle(Directory bundle) async {
Chris Bracken7a093162017-03-03 17:50:46 -0800542 final List<Map<String, String>> services = <Map<String, String>>[];
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200543 printTrace('Trying to resolve native pub services.');
Devon Carew67046f92016-02-20 22:00:11 -0800544
545 // Step 1: Parse the service configuration yaml files present in the service
546 // pub packages.
547 await parseServiceConfigs(services);
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200548 printTrace('Found ${services.length} service definition(s).');
Devon Carew67046f92016-02-20 22:00:11 -0800549
550 // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200551 final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks'));
Devon Carew67046f92016-02-20 22:00:11 -0800552 await _copyServiceFrameworks(services, frameworksDirectory);
553
554 // Step 3: Copy the service definitions manifest at the correct spot for
555 // xcodebuild to pick up.
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200556 final File manifestFile = fs.file(fs.path.join(bundle.path, 'ServiceDefinitions.json'));
Devon Carew67046f92016-02-20 22:00:11 -0800557 _copyServiceDefinitionsManifest(services, manifestFile);
558}
559
Ian Hicksond745e202016-03-12 00:32:34 -0800560Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800561 printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'.");
Devon Carew67046f92016-02-20 22:00:11 -0800562 frameworksDirectory.createSync(recursive: true);
563 for (Map<String, String> service in services) {
Chris Bracken7a093162017-03-03 17:50:46 -0800564 final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
565 final File dylib = fs.file(dylibPath);
Alexandre Ardhuin1fce14a2017-10-22 18:11:36 +0200566 printTrace('Copying ${dylib.path} into bundle.');
Devon Carew67046f92016-02-20 22:00:11 -0800567 if (!dylib.existsSync()) {
568 printError("The service dylib '${dylib.path}' does not exist.");
569 continue;
570 }
571 // Shell out so permissions on the dylib are preserved.
Todd Volkert60c5ffc2017-04-25 17:23:00 -0700572 await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
Devon Carew67046f92016-02-20 22:00:11 -0800573 }
574}
575
Chris Brackencdbdafa2018-05-03 19:40:16 -0700576/// The path of the Xcode project file.
577String _getPbxProjPath(String appPath) => fs.path.join(fs.currentDirectory.path, appPath, 'Runner.xcodeproj', 'project.pbxproj');
578
Devon Carew67046f92016-02-20 22:00:11 -0800579void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
580 printTrace("Creating service definitions manifest at '${manifest.path}'");
Chris Bracken7a093162017-03-03 17:50:46 -0800581 final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{
Devon Carew67046f92016-02-20 22:00:11 -0800582 'name': service['name'],
583 // Since we have already moved it to the Frameworks directory. Strip away
584 // the directory and basenames.
Michael Goderbauer5e54fd52017-02-13 17:45:50 -0800585 'framework': fs.path.basenameWithoutExtension(service['ios-framework'])
Devon Carew67046f92016-02-20 22:00:11 -0800586 }).toList();
Jason Simmons466d1542018-03-12 11:06:32 -0700587 final Map<String, dynamic> jsonObject = <String, dynamic>{ 'services' : jsonServices };
Todd Volkertd820e5f2018-05-03 22:27:29 -0700588 manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.WRITE, flush: true); // ignore: deprecated_member_use
Devon Carew67046f92016-02-20 22:00:11 -0800589}
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100590
Chris Brackencdbdafa2018-05-03 19:40:16 -0700591Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async {
592 final File xcodeProjectFile = fs.file(_getPbxProjPath(appPath));
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100593 assert(await xcodeProjectFile.exists());
594 final List<String> lines = await xcodeProjectFile.readAsLines();
595
596 if (lines.any((String line) => line.contains('path = Flutter/flutter_assets')))
597 return true;
598
Alexandre Ardhuin841d5d72018-02-01 07:51:26 +0100599 const String l1 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };';
600 const String l2 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };';
601 const String l3 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };';
602 const String l4 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };';
603 const String l5 = ' 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,';
604 const String l6 = ' 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,';
605 const String l7 = ' 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,';
606 const String l8 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,';
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100607
608
609 printStatus("Upgrading project.pbxproj of $app' to include the "
610 "'flutter_assets' directory");
611
612 if (!lines.contains(l1) || !lines.contains(l3) ||
613 !lines.contains(l5) || !lines.contains(l7)) {
614 printError('Automatic upgrade of project.pbxproj failed.');
Chris Brackencdbdafa2018-05-03 19:40:16 -0700615 printError(' To manually upgrade, open ${xcodeProjectFile.path}:');
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100616 printError(' Add the following line in the "PBXBuildFile" section');
617 printError(l2);
618 printError(' Add the following line in the "PBXFileReference" section');
619 printError(l4);
620 printError(' Add the following line in the "children" list of the "Flutter" group in the "PBXGroup" section');
621 printError(l6);
622 printError(' Add the following line in the "files" list of "Resources" in the "PBXResourcesBuildPhase" section');
623 printError(l8);
624 return false;
625 }
626
627 lines.insert(lines.indexOf(l1) + 1, l2);
628 lines.insert(lines.indexOf(l3) + 1, l4);
629 lines.insert(lines.indexOf(l5) + 1, l6);
630 lines.insert(lines.indexOf(l7) + 1, l8);
631
Alexandre Ardhuin841d5d72018-02-01 07:51:26 +0100632 const String l9 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB71CF902C7004384FC /* app.flx */; };';
633 const String l10 = ' 9740EEB71CF902C7004384FC /* app.flx */ = {isa = PBXFileReference; lastKnownFileType = file; name = app.flx; path = Flutter/app.flx; sourceTree = "<group>"; };';
634 const String l11 = ' 9740EEB71CF902C7004384FC /* app.flx */,';
635 const String l12 = ' 9740EEBB1CF902C7004384FC /* app.flx in Resources */,';
Sarah Zakarias73552ec2017-12-18 18:58:44 +0100636
637 if (lines.contains(l9)) {
638 printStatus('Removing app.flx from project.pbxproj since it has been '
639 'replaced with flutter_assets.');
640 lines.remove(l9);
641 lines.remove(l10);
642 lines.remove(l11);
643 lines.remove(l12);
644 }
Sarah Zakarias5e18c072017-12-14 17:27:25 +0100645
646 final StringBuffer buffer = new StringBuffer();
647 lines.forEach(buffer.writeln);
648 await xcodeProjectFile.writeAsString(buffer.toString());
649 return true;
Chris Bracken7fb78522017-12-15 18:13:10 -0800650}