From 7b1aec70ae7ad0e2bbd2344cd9fb33ba4737bb5b Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 9 Mar 2021 11:45:16 -0800 Subject: [PATCH] Move iOS codesigning into assemble build target (#77664) --- packages/flutter_tools/bin/xcode_backend.sh | 16 +- .../lib/src/build_system/targets/common.dart | 6 + .../lib/src/build_system/targets/ios.dart | 56 ++-- .../build_system/targets/ios_test.dart | 262 +++++++++++------- .../ios_content_validation_test.dart | 3 - 5 files changed, 222 insertions(+), 121 deletions(-) diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh index 6e0185542e..008df973db 100755 --- a/packages/flutter_tools/bin/xcode_backend.sh +++ b/packages/flutter_tools/bin/xcode_backend.sh @@ -165,6 +165,11 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr code_size_directory="-dCodeSizeDirectory=${CODE_SIZE_DIRECTORY}" fi + local codesign_identity_flag="" + if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" && "${CODE_SIGNING_REQUIRED:-}" != "NO" ]]; then + codesign_identity_flag="-dCodesignIdentity=${EXPANDED_CODE_SIGN_IDENTITY}" + fi + RunCommand "${FLUTTER_ROOT}/bin/flutter" \ ${verbose_flag} \ ${flutter_engine_flag} \ @@ -183,6 +188,7 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr -dTrackWidgetCreation="${TRACK_WIDGET_CREATION}" \ -dDartObfuscation="${DART_OBFUSCATION}" \ -dEnableBitcode="${bitcode_flag}" \ + "${codesign_identity_flag}" \ ${bundle_sksl_path} \ ${code_size_directory} \ --ExtraGenSnapshotOptions="${EXTRA_GEN_SNAPSHOT_OPTIONS}" \ @@ -214,15 +220,7 @@ EmbedFlutterFrameworks() { # Embed the actual Flutter.framework that the Flutter app expects to run against, # which could be a local build or an arch/type specific build. - - # Copy Xcode behavior and don't copy over headers or modules. - RunCommand rsync -av --delete --filter "- .DS_Store" --filter "- Headers" --filter "- Modules" "${BUILT_PRODUCTS_DIR}/Flutter.framework" "${xcode_frameworks_dir}/" - - # Sign the binaries we moved. - if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ]]; then - RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/App.framework/App" - RunCommand codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter" - fi + RunCommand rsync -av --delete --filter "- .DS_Store" "${BUILT_PRODUCTS_DIR}/Flutter.framework" "${xcode_frameworks_dir}/" AddObservatoryBonjourService } diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index 44c7614aeb..0fcda20047 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -80,6 +80,12 @@ const String kDartObfuscation = 'DartObfuscation'; /// An output directory where one or more code-size measurements may be written. const String kCodeSizeDirectory = 'CodeSizeDirectory'; +/// SHA identifier of the Apple developer code signing identity. +/// +/// Same as EXPANDED_CODE_SIGN_IDENTITY Xcode build setting. +/// Also discoverable via `security find-identity -p codesigning`. +const String kCodesignIdentity = 'CodesignIdentity'; + /// Copies the pre-built flutter bundle. // This is a one-off rule for implementing build bundle in terms of assemble. class CopyFlutterBundle extends Target { diff --git a/packages/flutter_tools/lib/src/build_system/targets/ios.dart b/packages/flutter_tools/lib/src/build_system/targets/ios.dart index 398cff9dfa..be5cccaa0c 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -11,7 +11,6 @@ import '../../base/build.dart'; import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/io.dart'; -import '../../base/process.dart'; import '../../build_info.dart'; import '../../globals.dart' as globals hide fs, logger, processManager, artifacts; import '../../macos/xcode.dart'; @@ -231,14 +230,11 @@ class DebugUniversalFramework extends Target { .childDirectory('App.framework') .childFile('App'); environment.buildDir.createSync(recursive: true); - final RunResult createFrameworkResult = await createStubAppFramework( + await _createStubAppFramework( output, - environment.defines[kSdkRoot], + environment, iosArchNames, ); - if (createFrameworkResult.exitCode != 0) { - throw Exception('Failed to create App.framework.'); - } } } @@ -283,18 +279,19 @@ abstract class UnpackIOS extends Target { if (environment.defines[kBitcodeFlag] == null) { throw MissingDefineException(kBitcodeFlag, name); } - await _copyFramework(environment); + _copyFramework(environment); final File frameworkBinary = environment.outputDir.childDirectory('Flutter.framework').childFile('Flutter'); final String frameworkBinaryPath = frameworkBinary.path; if (!frameworkBinary.existsSync()) { throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin'); } - await _thinFramework(environment, frameworkBinaryPath); - await _bitcodeStripFramework(environment, frameworkBinaryPath); + _thinFramework(environment, frameworkBinaryPath); + _bitcodeStripFramework(environment, frameworkBinaryPath); + _signFramework(environment, frameworkBinaryPath, buildMode); } - Future _copyFramework(Environment environment) async { + void _copyFramework(Environment environment) { final Directory sdkRoot = environment.fileSystem.directory(environment.defines[kSdkRoot]); final EnvironmentType environmentType = environmentTypeFromSdkroot(sdkRoot); final String basePath = environment.artifacts.getArtifactPath( @@ -322,7 +319,7 @@ abstract class UnpackIOS extends Target { } /// Destructively thin Flutter.framework to include only the specified architectures. - Future _thinFramework(Environment environment, String frameworkBinaryPath) async { + void _thinFramework(Environment environment, String frameworkBinaryPath) { final String archs = environment.defines[kIosArchs]; final List archList = archs.split(' ').toList(); final ProcessResult infoResult = environment.processManager.runSync([ @@ -368,7 +365,7 @@ abstract class UnpackIOS extends Target { } /// Destructively strip bitcode from the framework, if needed. - Future _bitcodeStripFramework(Environment environment, String frameworkBinaryPath) async { + void _bitcodeStripFramework(Environment environment, String frameworkBinaryPath) { if (environment.defines[kBitcodeFlag] == 'true') { return; } @@ -460,6 +457,7 @@ abstract class IosAssetBundle extends Target { } final BuildMode buildMode = getBuildModeForName(environment.defines[kBuildMode]); final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework'); + final String frameworkBinaryPath = frameworkDirectory.childFile('App').path; final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets'); frameworkDirectory.createSync(recursive: true); assetDirectory.createSync(); @@ -470,7 +468,7 @@ abstract class IosAssetBundle extends Target { environment.buildDir .childDirectory('App.framework') .childFile('App') - .copySync(frameworkDirectory.childFile('App').path); + .copySync(frameworkBinaryPath); final String vmSnapshotData = environment.artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug); final String isolateSnapshotData = environment.artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: BuildMode.debug); @@ -482,7 +480,7 @@ abstract class IosAssetBundle extends Target { .copySync(assetDirectory.childFile('isolate_snapshot_data').path); } else { environment.buildDir.childDirectory('App.framework').childFile('App') - .copySync(frameworkDirectory.childFile('App').path); + .copySync(frameworkBinaryPath); } // Copy the assets. @@ -512,6 +510,8 @@ abstract class IosAssetBundle extends Target { .copySync(environment.outputDir .childDirectory('App.framework') .childFile('Info.plist').path); + + _signFramework(environment, frameworkBinaryPath, buildMode); } } @@ -576,7 +576,7 @@ class ReleaseIosApplicationBundle extends IosAssetBundle { /// This framework needs to exist for the Xcode project to link/bundle, /// but it isn't actually executed. To generate something valid, we compile a trivial /// constant. -Future createStubAppFramework(File outputFile, String sdkRoot, +Future _createStubAppFramework(File outputFile, Environment environment, Set iosArchNames) async { try { outputFile.createSync(recursive: true); @@ -592,7 +592,8 @@ Future createStubAppFramework(File outputFile, String sdkRoot, static const int Moo = 88; '''); - return await globals.xcode.clang([ + final String sdkRoot = environment.defines[kSdkRoot]; + await globals.xcode.clang([ '-x', 'c', for (String arch in iosArchNames) ...['-arch', arch], @@ -616,4 +617,27 @@ Future createStubAppFramework(File outputFile, String sdkRoot, throwToolExit('Failed to create App.framework stub at ${outputFile.path}: $e'); } } + + _signFramework(environment, outputFile.path, BuildMode.debug); +} + +void _signFramework(Environment environment, String binaryPath, BuildMode buildMode) { + final String codesignIdentity = environment.defines[kCodesignIdentity]; + if (codesignIdentity == null || codesignIdentity.isEmpty) { + return; + } + final ProcessResult result = environment.processManager.runSync([ + 'codesign', + '--force', + '--sign', + codesignIdentity, + if (buildMode != BuildMode.release) ...[ + // Mimic Xcode's timestamp codesigning behavior on non-release binaries. + '--timestamp=none', + ], + binaryPath, + ]); + if (result.exitCode != 0) { + throw Exception('Failed to codesign $binaryPath with identity $codesignIdentity.\n${result.stderr}'); + } } diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart index 5f30231395..aad8c0e10b 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/ios_test.dart @@ -95,6 +95,7 @@ void main() { ); await const DebugUniversalFramework().build(environment); + expect(processManager.hasRemainingExpectations, isFalse); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, @@ -104,6 +105,7 @@ void main() { testUsingContext('DebugIosApplicationBundle', () async { environment.inputs[kBundleSkSLPath] = 'bundle.sksl'; environment.defines[kBuildMode] = 'debug'; + environment.defines[kCodesignIdentity] = 'ABC123'; // Precompiled dart data fileSystem.file(artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug)) @@ -120,7 +122,7 @@ void main() { environment.buildDir.childFile('app.dill').createSync(recursive: true); // Stub framework environment.buildDir - .childDirectory('App.framework') + .childDirectory('App.framework') .childFile('App') .createSync(recursive: true); // sksl bundle @@ -134,10 +136,23 @@ void main() { } )); - await const DebugIosApplicationBundle().build(environment); - final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework'); - expect(frameworkDirectory.childFile('App'), exists); + final File frameworkDirectoryBinary = frameworkDirectory.childFile('App'); + processManager.addCommand( + FakeCommand(command: [ + 'codesign', + '--force', + '--sign', + 'ABC123', + '--timestamp=none', + frameworkDirectoryBinary.path, + ]), + ); + + await const DebugIosApplicationBundle().build(environment); + expect(processManager.hasRemainingExpectations, isFalse); + + expect(frameworkDirectoryBinary, exists); expect(frameworkDirectory.childFile('Info.plist'), exists); final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets'); @@ -151,6 +166,7 @@ void main() { testUsingContext('ReleaseIosApplicationBundle', () async { environment.defines[kBuildMode] = 'release'; + environment.defines[kCodesignIdentity] = 'ABC123'; // Project info fileSystem.file('pubspec.yaml').writeAsStringSync('name: hello'); @@ -165,10 +181,23 @@ void main() { .childFile('App') .createSync(recursive: true); - await const ReleaseIosApplicationBundle().build(environment); final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework'); - expect(frameworkDirectory.childFile('App'), exists); + final File frameworkDirectoryBinary = frameworkDirectory.childFile('App'); + processManager.addCommand( + FakeCommand(command: [ + 'codesign', + '--force', + '--sign', + 'ABC123', + frameworkDirectoryBinary.path, + ]), + ); + + await const ReleaseIosApplicationBundle().build(environment); + expect(processManager.hasRemainingExpectations, isFalse); + + expect(frameworkDirectoryBinary, exists); expect(frameworkDirectory.childFile('Info.plist'), exists); final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets'); @@ -205,6 +234,7 @@ void main() { contains('release/profile builds are only supported for physical devices.'), ) )); + expect(processManager.hasRemainingExpectations, isFalse); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, @@ -233,19 +263,25 @@ void main() { contains('required define SdkRoot but it was not provided'), ) )); + expect(processManager.hasRemainingExpectations, isFalse); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, Platform: () => macPlatform, }); - group('copy, thin, and bitcode strip engine Flutter.framework', () { + group('copies Flutter.framework', () { Directory outputDir; + File binary; FakeCommand copyPhysicalFrameworkCommand; + FakeCommand lipoCommandNonFatResult; + FakeCommand lipoVerifyArm64Command; + FakeCommand bitcodeStripCommand; setUp(() { final FileSystem fileSystem = MemoryFileSystem.test(); outputDir = fileSystem.directory('output'); + binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter'); copyPhysicalFrameworkCommand = FakeCommand(command: [ 'rsync', '-av', @@ -255,10 +291,31 @@ void main() { 'Artifact.flutterFramework.TargetPlatform.ios.debug.EnvironmentType.physical', outputDir.path, ]); + + lipoCommandNonFatResult = FakeCommand(command: [ + 'lipo', + '-info', + binary.path, + ], stdout: 'Non-fat file:'); + + lipoVerifyArm64Command = FakeCommand(command: [ + 'lipo', + binary.path, + '-verify_arch', + 'arm64', + ]); + + bitcodeStripCommand = FakeCommand(command: [ + 'xcrun', + 'bitcode_strip', + binary.path, + '-m', + '-o', + binary.path, + ]); }); testWithoutContext('iphonesimulator', () async { - final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter'); final Environment environment = Environment.test( fileSystem.currentDirectory, processManager: processManager, @@ -273,7 +330,7 @@ void main() { }, ); - processManager.addCommand( + processManager.addCommands([ FakeCommand(command: [ 'rsync', '-av', @@ -285,24 +342,14 @@ void main() { ], onRun: () => binary.createSync(recursive: true), ), - ); - - processManager.addCommand( - FakeCommand(command: [ - 'lipo', - '-info', - binary.path, - ], stdout: 'Non-fat file:'), - ); - - processManager.addCommand( + lipoCommandNonFatResult, FakeCommand(command: [ 'lipo', binary.path, '-verify_arch', 'x86_64', ]), - ); + ]); await const DebugUnpackIOS().build(environment); expect(logger.traceText, contains('Skipping lipo for non-fat file output/Flutter.framework/Flutter')); @@ -334,7 +381,7 @@ void main() { }); testWithoutContext('fails when requested archs missing from framework', () async { - final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); + binary.createSync(recursive: true); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -350,16 +397,13 @@ void main() { }, ); - processManager.addCommand(copyPhysicalFrameworkCommand); - processManager.addCommand( + processManager.addCommands([ + copyPhysicalFrameworkCommand, FakeCommand(command: [ 'lipo', '-info', binary.path, ], stdout: 'Architectures in the fat file:'), - ); - - processManager.addCommand( FakeCommand(command: [ 'lipo', binary.path, @@ -367,7 +411,7 @@ void main() { 'arm64', 'armv7', ], exitCode: 1), - ); + ]); await expectLater( const DebugUnpackIOS().build(environment), @@ -379,7 +423,7 @@ void main() { }); testWithoutContext('fails when lipo extract fails', () async { - final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); + binary.createSync(recursive: true); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -395,16 +439,13 @@ void main() { }, ); - processManager.addCommand(copyPhysicalFrameworkCommand); - processManager.addCommand( + processManager.addCommands([ + copyPhysicalFrameworkCommand, FakeCommand(command: [ 'lipo', '-info', binary.path, ], stdout: 'Architectures in the fat file:'), - ); - - processManager.addCommand( FakeCommand(command: [ 'lipo', binary.path, @@ -412,9 +453,6 @@ void main() { 'arm64', 'armv7', ]), - ); - - processManager.addCommand( FakeCommand(command: [ 'lipo', '-output', @@ -426,7 +464,7 @@ void main() { binary.path, ], exitCode: 1, stderr: 'lipo error'), - ); + ]); await expectLater( const DebugUnpackIOS().build(environment), @@ -438,7 +476,7 @@ void main() { }); testWithoutContext('skips thin framework', () async { - final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); + binary.createSync(recursive: true); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -454,23 +492,11 @@ void main() { }, ); - processManager.addCommand(copyPhysicalFrameworkCommand); - processManager.addCommand( - FakeCommand(command: [ - 'lipo', - '-info', - binary.path, - ], stdout: 'Non-fat file:'), - ); - - processManager.addCommand( - FakeCommand(command: [ - 'lipo', - binary.path, - '-verify_arch', - 'arm64', - ]), - ); + processManager.addCommands([ + copyPhysicalFrameworkCommand, + lipoCommandNonFatResult, + lipoVerifyArm64Command, + ]); await const DebugUnpackIOS().build(environment); expect(logger.traceText, contains('Skipping lipo for non-fat file output/Flutter.framework/Flutter')); @@ -479,7 +505,7 @@ void main() { }); testWithoutContext('thins fat framework', () async { - final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); + binary.createSync(recursive: true); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -495,16 +521,13 @@ void main() { }, ); - processManager.addCommand(copyPhysicalFrameworkCommand); - processManager.addCommand( + processManager.addCommands([ + copyPhysicalFrameworkCommand, FakeCommand(command: [ 'lipo', '-info', binary.path, ], stdout: 'Architectures in the fat file:'), - ); - - processManager.addCommand( FakeCommand(command: [ 'lipo', binary.path, @@ -512,9 +535,6 @@ void main() { 'arm64', 'armv7', ]), - ); - - processManager.addCommand( FakeCommand(command: [ 'lipo', '-output', @@ -525,14 +545,14 @@ void main() { 'armv7', binary.path, ]), - ); + ]); await const DebugUnpackIOS().build(environment); expect(processManager.hasRemainingExpectations, isFalse); }); testWithoutContext('fails when bitcode strip fails', () async { - final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); + binary.createSync(recursive: true); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -550,17 +570,8 @@ void main() { processManager.addCommands([ copyPhysicalFrameworkCommand, - FakeCommand(command: [ - 'lipo', - '-info', - binary.path, - ], stdout: 'Non-fat file:'), - FakeCommand(command: [ - 'lipo', - binary.path, - '-verify_arch', - 'arm64', - ]), + lipoCommandNonFatResult, + lipoVerifyArm64Command, FakeCommand(command: [ 'xcrun', 'bitcode_strip', @@ -583,7 +594,7 @@ void main() { }); testWithoutContext('strips framework', () async { - final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); + binary.createSync(recursive: true); final Environment environment = Environment.test( fileSystem.currentDirectory, @@ -601,23 +612,88 @@ void main() { processManager.addCommands([ copyPhysicalFrameworkCommand, + lipoCommandNonFatResult, + lipoVerifyArm64Command, + bitcodeStripCommand, + ]); + await const DebugUnpackIOS().build(environment); + + expect(processManager.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('fails when codesign fails', () async { + binary.createSync(recursive: true); + + final Environment environment = Environment.test( + fileSystem.currentDirectory, + processManager: processManager, + artifacts: artifacts, + logger: logger, + fileSystem: fileSystem, + outputDir: outputDir, + defines: { + kIosArchs: 'arm64', + kSdkRoot: 'path/to/iPhoneOS.sdk', + kBitcodeFlag: '', + kCodesignIdentity: 'ABC123', + }, + ); + + processManager.addCommands([ + copyPhysicalFrameworkCommand, + lipoCommandNonFatResult, + lipoVerifyArm64Command, + bitcodeStripCommand, FakeCommand(command: [ - 'lipo', - '-info', + 'codesign', + '--force', + '--sign', + 'ABC123', + '--timestamp=none', binary.path, - ], stdout: 'Non-fat file:'), + ], exitCode: 1, stderr: 'codesign error'), + ]); + + await expectLater( + const DebugUnpackIOS().build(environment), + throwsA(isA().having( + (Exception exception) => exception.toString(), + 'description', + contains('Failed to codesign output/Flutter.framework/Flutter with identity ABC123.\ncodesign error'), + ))); + + expect(processManager.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('codesigns framework', () async { + binary.createSync(recursive: true); + + final Environment environment = Environment.test( + fileSystem.currentDirectory, + processManager: processManager, + artifacts: artifacts, + logger: logger, + fileSystem: fileSystem, + outputDir: outputDir, + defines: { + kIosArchs: 'arm64', + kSdkRoot: 'path/to/iPhoneOS.sdk', + kBitcodeFlag: '', + kCodesignIdentity: 'ABC123', + }, + ); + + processManager.addCommands([ + copyPhysicalFrameworkCommand, + lipoCommandNonFatResult, + lipoVerifyArm64Command, + bitcodeStripCommand, FakeCommand(command: [ - 'lipo', - binary.path, - '-verify_arch', - 'arm64', - ]), - FakeCommand(command: [ - 'xcrun', - 'bitcode_strip', - binary.path, - '-m', - '-o', + 'codesign', + '--force', + '--sign', + 'ABC123', + '--timestamp=none', binary.path, ]), ]); diff --git a/packages/flutter_tools/test/integration.shard/ios_content_validation_test.dart b/packages/flutter_tools/test/integration.shard/ios_content_validation_test.dart index 35553f88bc..c5aefdede3 100644 --- a/packages/flutter_tools/test/integration.shard/ios_content_validation_test.dart +++ b/packages/flutter_tools/test/integration.shard/ios_content_validation_test.dart @@ -119,9 +119,6 @@ void main() { expect(vmSnapshot.existsSync(), buildMode == BuildMode.debug); - expect(outputFlutterFramework.childDirectory('Headers'), isNot(exists)); - expect(outputFlutterFramework.childDirectory('Modules'), isNot(exists)); - // Archiving should contain a bitcode blob, but not building. // This mimics Xcode behavior and prevents a developer from having to install a // 300+MB app.