diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh index 6f526695fd..fc1539aeda 100755 --- a/packages/flutter_tools/bin/xcode_backend.sh +++ b/packages/flutter_tools/bin/xcode_backend.sh @@ -4,7 +4,9 @@ # found in the LICENSE file. RunCommand() { - echo "♦ $*" + if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then + echo "♦ $*" + fi "$@" return $? } diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index b5840523da..f98dd46add 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart @@ -210,7 +210,11 @@ class BuildableIOSApp extends IOSApp { final String appDirectory; - /// Build settings of the app's XCode project. + /// Build settings of the app's Xcode project. + /// + /// These are the build settings as specified in the Xcode project files. + /// + /// Build settings may change depending on the parameters passed while building. final Map buildSettings; @override diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index ee0c312339..3055567a9c 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -76,7 +76,7 @@ class BuildIOSCommand extends BuildSubCommand { ); if (!result.success) { - await diagnoseXcodeBuildFailure(result, app); + await diagnoseXcodeBuildFailure(result); throwToolExit('Encountered error while building for $logTarget.'); } diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 1fc495cf77..3867a71a21 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -172,7 +172,7 @@ class IOSDevice extends Device { ); if (!buildResult.success) { printError('Could not build the precompiled application for the device.'); - await diagnoseXcodeBuildFailure(buildResult, app); + await diagnoseXcodeBuildFailure(buildResult); printError(''); return new LaunchResult.failed(); } diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index db10ef4362..a166b61d5c 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -277,26 +277,51 @@ Future buildXcodeProject({ ); } - final List commands = [ + final Status cleanStatus = + logger.startProgress('Running Xcode clean...', expectSlowOperation: true); + final RunResult cleanResult = await runAsync( + [ + '/usr/bin/env', + 'xcrun', + 'xcodebuild', + 'clean', + '-configuration', configuration, + ], + workingDirectory: app.appDirectory, + ); + cleanStatus.stop(); + if (cleanResult.exitCode != 0) { + throwToolExit('Xcode failed to clean\n${cleanResult.stderr}'); + } + + final List buildCommands = [ '/usr/bin/env', 'xcrun', 'xcodebuild', - 'clean', 'build', '-configuration', configuration, 'ONLY_ACTIVE_ARCH=YES', ]; + if (logger.isVerbose) { + // An environment variable to be passed to xcode_backend.sh determining + // whether to echo back executed commands. + buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES'); + } else { + // This will print warnings and errors only. + buildCommands.add('-quiet'); + } + if (developmentTeam != null) { - commands.add('DEVELOPMENT_TEAM=$developmentTeam'); - commands.add('-allowProvisioningUpdates'); - commands.add('-allowProvisioningDeviceRegistration'); + buildCommands.add('DEVELOPMENT_TEAM=$developmentTeam'); + buildCommands.add('-allowProvisioningUpdates'); + buildCommands.add('-allowProvisioningDeviceRegistration'); } final List contents = fs.directory(app.appDirectory).listSync(); for (FileSystemEntity entity in contents) { if (fs.path.extension(entity.path) == '.xcworkspace') { - commands.addAll([ + buildCommands.addAll([ '-workspace', fs.path.basename(entity.path), '-scheme', scheme, 'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}', @@ -306,13 +331,13 @@ Future buildXcodeProject({ } if (buildForDevice) { - commands.addAll(['-sdk', 'iphoneos', '-arch', 'arm64']); + buildCommands.addAll(['-sdk', 'iphoneos', '-arch', 'arm64']); } else { - commands.addAll(['-sdk', 'iphonesimulator', '-arch', 'x86_64']); + buildCommands.addAll(['-sdk', 'iphonesimulator', '-arch', 'x86_64']); } if (!codesign) { - commands.addAll( + buildCommands.addAll( [ 'CODE_SIGNING_ALLOWED=NO', 'CODE_SIGNING_REQUIRED=NO', @@ -321,49 +346,61 @@ Future buildXcodeProject({ ); } - final Status status = logger.startProgress('Running Xcode build...', expectSlowOperation: true); - final RunResult result = await runAsync( - commands, + final Status buildStatus = + logger.startProgress('Running Xcode build...', expectSlowOperation: true); + final RunResult buildResult = await runAsync( + buildCommands, workingDirectory: app.appDirectory, allowReentrantFlutter: true ); - status.stop(); - if (result.exitCode != 0) { + buildStatus.stop(); + + // Run -showBuildSettings again but with the exact same parameters as the build. + final Map buildSettings = parseXcodeBuildSettings(runCheckedSync( + new List.from(buildCommands)..add('-showBuildSettings'), + workingDirectory: app.appDirectory, + )); + + if (buildResult.exitCode != 0) { printStatus('Failed to build iOS app'); - if (result.stderr.isNotEmpty) { + if (buildResult.stderr.isNotEmpty) { printStatus('Error output from Xcode build:\n↳'); - printStatus(result.stderr, indent: 4); + printStatus(buildResult.stderr, indent: 4); } - if (result.stdout.isNotEmpty) { + if (buildResult.stdout.isNotEmpty) { printStatus('Xcode\'s output:\n↳'); - printStatus(result.stdout, indent: 4); + printStatus(buildResult.stdout, indent: 4); } return new XcodeBuildResult( success: false, - stdout: result.stdout, - stderr: result.stderr, + stdout: buildResult.stdout, + stderr: buildResult.stderr, xcodeBuildExecution: new XcodeBuildExecution( - commands, - app.appDirectory, + buildCommands: buildCommands, + appDirectory: app.appDirectory, buildForPhysicalDevice: buildForDevice, + buildSettings: buildSettings, ), ); } else { - // Look for 'clean build/-/Runner.app'. - final RegExp regexp = new RegExp(r' clean (.*\.app)$', multiLine: true); - final Match match = regexp.firstMatch(result.stdout); + final String expectedOutputDirectory = fs.path.join( + buildSettings['TARGET_BUILD_DIR'], + buildSettings['WRAPPER_NAME'], + ); + String outputDir; - if (match != null) { - final String actualOutputDir = match.group(1).replaceAll('\\ ', ' '); + if (fs.isDirectorySync(expectedOutputDirectory)) { // Copy app folder to a place where other tools can find it without knowing // the BuildInfo. - outputDir = actualOutputDir.replaceFirst('/$configuration-', '/'); + outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/'); if (fs.isDirectorySync(outputDir)) { // Previous output directory might have incompatible artifacts // (for example, kernel binary files produced from previous `--preview-dart-2` run). fs.directory(outputDir).deleteSync(recursive: true); } - copyDirectorySync(fs.directory(actualOutputDir), fs.directory(outputDir)); + copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir)); + } else { + printError('Build succeeded but the expected app at $expectedOutputDirectory not found'); } return new XcodeBuildResult(success: true, output: outputDir); } @@ -378,8 +415,7 @@ String readGeneratedXcconfig(String appPath) { return generatedXcconfigFile.readAsStringSync(); } -Future diagnoseXcodeBuildFailure( - XcodeBuildResult result, BuildableIOSApp app) async { +Future diagnoseXcodeBuildFailure(XcodeBuildResult result) async { if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && result.stdout?.contains('BCEROR') == true && @@ -393,14 +429,15 @@ Future diagnoseXcodeBuildFailure( // * PROVISIONING_PROFILE (manual signing) if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && - app.buildSettings != null && - !['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(app.buildSettings.containsKey)) { + !['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any( + result.xcodeBuildExecution.buildSettings.containsKey) + ) { printError(noDevelopmentTeamInstruction, emphasis: true); return; } if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && - app.id.contains('com.example')) { + result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER'].contains('com.example')) { printError(''); printError('It appears that your application still contains the default signing identifier.'); printError("Try replacing 'com.example' with your signing id in Xcode:"); @@ -441,10 +478,11 @@ class XcodeBuildResult { /// Describes an invocation of a Xcode build command. class XcodeBuildExecution { XcodeBuildExecution( - this.buildCommands, - this.appDirectory, { + @required this.buildCommands, + @required this.appDirectory, @required this.buildForPhysicalDevice, + @required this.buildSettings, } ); @@ -452,6 +490,8 @@ class XcodeBuildExecution { final List buildCommands; final String appDirectory; final bool buildForPhysicalDevice; + /// The build settings corresponding to the [buildCommands] invocation. + final Map buildSettings; } final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*'); diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 7dbeca7edd..47b3d5c602 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -72,8 +72,12 @@ Map getXcodeBuildSettings(String xcodeProjPath, String target) { final String out = runCheckedSync([ '/usr/bin/xcodebuild', '-project', absProjPath, '-target', target, '-showBuildSettings' ]); + return parseXcodeBuildSettings(out); +} + +Map parseXcodeBuildSettings(String showBuildSettingsOutput) { final Map settings = {}; - for (String line in out.split('\n').where(_settingExpr.hasMatch)) { + for (String line in showBuildSettingsOutput.split('\n').where(_settingExpr.hasMatch)) { final Match match = _settingExpr.firstMatch(line); settings[match[1]] = match[2]; } diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart index 987c737cc9..7110f4e596 100644 --- a/packages/flutter_tools/test/ios/mac_test.dart +++ b/packages/flutter_tools/test/ios/mac_test.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:file/file.dart'; -import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult; import 'package:flutter_tools/src/ios/mac.dart'; @@ -243,15 +242,12 @@ void main() { }); group('Diagnose Xcode build failure', () { - BuildableIOSApp app; + Map buildSettings; setUp(() { - app = new BuildableIOSApp( - projectBundleId: 'test.app', - buildSettings: { - 'For our purposes': 'a non-empty build settings map is valid', - }, - ); + buildSettings = { + 'PRODUCT_BUNDLE_IDENTIFIER': 'test.app', + }; }); testUsingContext('No provisioning profile shows message', () async { @@ -313,13 +309,14 @@ Could not build the precompiled application for the device. Error launching application on iPhone.''', xcodeBuildExecution: new XcodeBuildExecution( - ['xcrun', 'xcodebuild', 'blah'], - '/blah/blah', - buildForPhysicalDevice: true + buildCommands: ['xcrun', 'xcodebuild', 'blah'], + appDirectory: '/blah/blah', + buildForPhysicalDevice: true, + buildSettings: buildSettings, ), ); - await diagnoseXcodeBuildFailure(buildResult, app); + await diagnoseXcodeBuildFailure(buildResult); expect( testLogger.errorText, contains('No Provisioning Profile was found for your project\'s Bundle Identifier or your device.'), @@ -393,13 +390,14 @@ Xcode's output: Could not build the precompiled application for the device.''', xcodeBuildExecution: new XcodeBuildExecution( - ['xcrun', 'xcodebuild', 'blah'], - '/blah/blah', - buildForPhysicalDevice: true + buildCommands: ['xcrun', 'xcodebuild', 'blah'], + appDirectory: '/blah/blah', + buildForPhysicalDevice: true, + buildSettings: buildSettings, ), ); - await diagnoseXcodeBuildFailure(buildResult, app); + await diagnoseXcodeBuildFailure(buildResult); expect( testLogger.errorText, contains('Building a deployable iOS app requires a selected Development Team with a Provisioning Profile'),