diff --git a/dev/devicelab/lib/framework/ios.dart b/dev/devicelab/lib/framework/ios.dart index 0cd00440881..dd0dbdc6617 100644 --- a/dev/devicelab/lib/framework/ios.dart +++ b/dev/devicelab/lib/framework/ios.dart @@ -42,7 +42,7 @@ Future containsBitcode(String pathToBinary) async { if (line.contains('segname __LLVM') && lines.length - index - 1 > 3) { final String emptyBitcodeMarker = lines .skip(index - 1) - .take(3) + .take(4) .firstWhere( (String line) => line.contains(' size 0x0000000000000001'), orElse: () => null, diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh index 5d87a23f00b..6e0185542e7 100755 --- a/packages/flutter_tools/bin/xcode_backend.sh +++ b/packages/flutter_tools/bin/xcode_backend.sh @@ -137,9 +137,8 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr local_engine_flag="--local-engine=${LOCAL_ENGINE}" flutter_framework="${FLUTTER_ENGINE}/out/${LOCAL_ENGINE}/Flutter.xcframework" fi - local bitcode_flag="" - if [[ "$ENABLE_BITCODE" == "YES" ]]; then + if [[ "$ENABLE_BITCODE" == "YES" && "$ACTION" == "install" ]]; then bitcode_flag="true" fi @@ -218,10 +217,6 @@ EmbedFlutterFrameworks() { # 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}/" - if [[ "$ACTION" != "install" || "$ENABLE_BITCODE" == "NO" ]]; then - # Strip bitcode from the destination unless archiving, or if bitcode is disabled entirely. - RunCommand "${DT_TOOLCHAIN_DIR}"/usr/bin/bitcode_strip "${BUILT_PRODUCTS_DIR}/Flutter.framework/Flutter" -r -o "${xcode_frameworks_dir}/Flutter.framework/Flutter" - fi # Sign the binaries we moved. if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ]]; then 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 428d838069a..398cff9dfa9 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -280,8 +280,18 @@ abstract class UnpackIOS extends Target { if (environment.defines[kIosArchs] == null) { throw MissingDefineException(kIosArchs, name); } + if (environment.defines[kBitcodeFlag] == null) { + throw MissingDefineException(kBitcodeFlag, name); + } await _copyFramework(environment); - await _thinFramework(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); } Future _copyFramework(Environment environment) async { @@ -312,37 +322,30 @@ abstract class UnpackIOS extends Target { } /// Destructively thin Flutter.framework to include only the specified architectures. - Future _thinFramework(Environment environment) async { - final Directory frameworkDirectory = environment.outputDir; - - final File flutterFramework = frameworkDirectory.childDirectory('Flutter.framework').childFile('Flutter'); - final String binaryPath = flutterFramework.path; - if (!flutterFramework.existsSync()) { - throw Exception('Binary $binaryPath does not exist, cannot thin'); - } + Future _thinFramework(Environment environment, String frameworkBinaryPath) async { final String archs = environment.defines[kIosArchs]; final List archList = archs.split(' ').toList(); final ProcessResult infoResult = environment.processManager.runSync([ 'lipo', '-info', - binaryPath, + frameworkBinaryPath, ]); final String lipoInfo = infoResult.stdout as String; final ProcessResult verifyResult = environment.processManager.runSync([ 'lipo', - binaryPath, + frameworkBinaryPath, '-verify_arch', ...archList ]); if (verifyResult.exitCode != 0) { - throw Exception('Binary $binaryPath does not contain $archs. Running lipo -info:\n$lipoInfo'); + throw Exception('Binary $frameworkBinaryPath does not contain $archs. Running lipo -info:\n$lipoInfo'); } // Skip thinning for non-fat executables. if (lipoInfo.startsWith('Non-fat file:')) { - environment.logger.printTrace('Skipping lipo for non-fat file $binaryPath'); + environment.logger.printTrace('Skipping lipo for non-fat file $frameworkBinaryPath'); return; } @@ -350,17 +353,36 @@ abstract class UnpackIOS extends Target { final ProcessResult extractResult = environment.processManager.runSync([ 'lipo', '-output', - binaryPath, + frameworkBinaryPath, for (final String arch in archList) ...[ '-extract', arch, ], - ...[binaryPath], + ...[frameworkBinaryPath], ]); if (extractResult.exitCode != 0) { - throw Exception('Failed to extract $archs for $binaryPath.\n${extractResult.stderr}\nRunning lipo -info:\n$lipoInfo'); + throw Exception('Failed to extract $archs for $frameworkBinaryPath.\n${extractResult.stderr}\nRunning lipo -info:\n$lipoInfo'); + } + } + + /// Destructively strip bitcode from the framework, if needed. + Future _bitcodeStripFramework(Environment environment, String frameworkBinaryPath) async { + if (environment.defines[kBitcodeFlag] == 'true') { + return; + } + final ProcessResult stripResult = environment.processManager.runSync([ + 'xcrun', + 'bitcode_strip', + frameworkBinaryPath, + '-m', // leave the bitcode marker. + '-o', + frameworkBinaryPath, + ]); + + if (stripResult.exitCode != 0) { + throw Exception('Failed to strip bitcode for $frameworkBinaryPath.\n${stripResult.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 2479e40b36d..5f302313950 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 @@ -239,7 +239,7 @@ void main() { Platform: () => macPlatform, }); - group('copy and thin engine Flutter.framework', () { + group('copy, thin, and bitcode strip engine Flutter.framework', () { Directory outputDir; FakeCommand copyPhysicalFrameworkCommand; @@ -269,6 +269,7 @@ void main() { defines: { kIosArchs: 'x86_64', kSdkRoot: 'path/to/iPhoneSimulator.sdk', + kBitcodeFlag: 'true', }, ); @@ -308,7 +309,7 @@ void main() { expect(processManager.hasRemainingExpectations, isFalse); }); - testWithoutContext('thinning fails when frameworks missing', () async { + testWithoutContext('fails when frameworks missing', () async { final Environment environment = Environment.test( fileSystem.currentDirectory, processManager: processManager, @@ -319,10 +320,11 @@ void main() { defines: { kIosArchs: 'arm64', kSdkRoot: 'path/to/iPhoneOS.sdk', + kBitcodeFlag: '', }, ); processManager.addCommand(copyPhysicalFrameworkCommand); - expect( + await expectLater( const DebugUnpackIOS().build(environment), throwsA(isA().having( (Exception exception) => exception.toString(), @@ -331,7 +333,7 @@ void main() { ))); }); - testWithoutContext('thinning fails when requested archs missing from framework', () async { + testWithoutContext('fails when requested archs missing from framework', () async { final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); final Environment environment = Environment.test( @@ -344,6 +346,7 @@ void main() { defines: { kIosArchs: 'arm64 armv7', kSdkRoot: 'path/to/iPhoneOS.sdk', + kBitcodeFlag: '', }, ); @@ -366,7 +369,7 @@ void main() { ], exitCode: 1), ); - expect( + await expectLater( const DebugUnpackIOS().build(environment), throwsA(isA().having( (Exception exception) => exception.toString(), @@ -375,7 +378,7 @@ void main() { ))); }); - testWithoutContext('thinning fails when lipo extract fails', () async { + testWithoutContext('fails when lipo extract fails', () async { final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); final Environment environment = Environment.test( @@ -388,6 +391,7 @@ void main() { defines: { kIosArchs: 'arm64 armv7', kSdkRoot: 'path/to/iPhoneOS.sdk', + kBitcodeFlag: '', }, ); @@ -424,7 +428,7 @@ void main() { stderr: 'lipo error'), ); - expect( + await expectLater( const DebugUnpackIOS().build(environment), throwsA(isA().having( (Exception exception) => exception.toString(), @@ -433,7 +437,7 @@ void main() { ))); }); - testWithoutContext('skips thin frameworks', () async { + testWithoutContext('skips thin framework', () async { final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); final Environment environment = Environment.test( @@ -446,6 +450,7 @@ void main() { defines: { kIosArchs: 'arm64', kSdkRoot: 'path/to/iPhoneOS.sdk', + kBitcodeFlag: 'true', }, ); @@ -473,7 +478,7 @@ void main() { expect(processManager.hasRemainingExpectations, isFalse); }); - testWithoutContext('thins fat frameworks', () async { + testWithoutContext('thins fat framework', () async { final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..createSync(recursive: true); final Environment environment = Environment.test( @@ -486,6 +491,7 @@ void main() { defines: { kIosArchs: 'arm64 armv7', kSdkRoot: 'path/to/iPhoneOS.sdk', + kBitcodeFlag: 'true', }, ); @@ -524,5 +530,100 @@ void main() { 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); + + 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: '', + }, + ); + + processManager.addCommands([ + copyPhysicalFrameworkCommand, + FakeCommand(command: [ + 'lipo', + '-info', + binary.path, + ], stdout: 'Non-fat file:'), + FakeCommand(command: [ + 'lipo', + binary.path, + '-verify_arch', + 'arm64', + ]), + FakeCommand(command: [ + 'xcrun', + 'bitcode_strip', + binary.path, + '-m', + '-o', + binary.path, + ], exitCode: 1, stderr: 'bitcode_strip error'), + ]); + + await expectLater( + const DebugUnpackIOS().build(environment), + throwsA(isA().having( + (Exception exception) => exception.toString(), + 'description', + contains('Failed to strip bitcode for output/Flutter.framework/Flutter.\nbitcode_strip error'), + ))); + + expect(processManager.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('strips framework', () async { + final File binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter')..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: '', + }, + ); + + processManager.addCommands([ + copyPhysicalFrameworkCommand, + FakeCommand(command: [ + 'lipo', + '-info', + binary.path, + ], stdout: 'Non-fat file:'), + FakeCommand(command: [ + 'lipo', + binary.path, + '-verify_arch', + 'arm64', + ]), + FakeCommand(command: [ + 'xcrun', + 'bitcode_strip', + binary.path, + '-m', + '-o', + binary.path, + ]), + ]); + await const DebugUnpackIOS().build(environment); + + expect(processManager.hasRemainingExpectations, isFalse); + }); }); } diff --git a/packages/flutter_tools/test/src/darwin_common.dart b/packages/flutter_tools/test/src/darwin_common.dart index b5bea318f30..ffb01203dbf 100644 --- a/packages/flutter_tools/test/src/darwin_common.dart +++ b/packages/flutter_tools/test/src/darwin_common.dart @@ -40,7 +40,7 @@ bool containsBitcode(String pathToBinary, ProcessManager processManager) { lines.asMap().forEach((int index, String line) { if (line.contains('segname __LLVM') && lines.length - index - 1 > 3) { final String emptyBitcodeMarker = - lines.skip(index - 1).take(3).firstWhere( + lines.skip(index - 1).take(4).firstWhere( (String line) => line.contains(' size 0x0000000000000001'), orElse: () => null, );