diff --git a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart index 726ea1708dc..051e191f5ae 100644 --- a/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart +++ b/dev/devicelab/bin/tasks/build_ios_framework_module_test.dart @@ -42,6 +42,21 @@ Future main() async { ); }); + // First, build the module in Debug to copy the debug version of Flutter.framework. + // This proves "flutter build ios-framework" re-copies the relevant Flutter.framework, + // otherwise building plugins with bitcode will fail linking because the debug version + // of Flutter.framework does not contain bitcode. + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: [ + 'ios', + '--debug', + '--no-codesign', + ], + ); + }); + // This builds all build modes' frameworks by default section('Build frameworks'); @@ -123,6 +138,7 @@ Future main() async { ); await _checkFrameworkArchs(appFrameworkPath, mode); + await _checkBitcode(appFrameworkPath, mode); final String aotSymbols = await dylibSymbols(appFrameworkPath); @@ -168,6 +184,7 @@ Future main() async { ); await _checkFrameworkArchs(engineFrameworkPath, mode); + await _checkBitcode(engineFrameworkPath, mode); checkFileExists(path.join( outputPath, @@ -211,6 +228,7 @@ Future main() async { 'device_info', ); await _checkFrameworkArchs(pluginFrameworkPath, mode); + await _checkBitcode(pluginFrameworkPath, mode); checkFileExists(path.join( outputPath, @@ -235,7 +253,7 @@ Future main() async { } } - section("Check all modes' have generated plugin registrant"); + section('Check all modes have generated plugin registrant'); for (final String mode in ['Debug', 'Profile', 'Release']) { final String registrantFrameworkPath = path.join( @@ -246,6 +264,7 @@ Future main() async { ); await _checkFrameworkArchs(registrantFrameworkPath, mode); + await _checkBitcode(registrantFrameworkPath, mode); checkFileExists(path.join( outputPath, @@ -310,3 +329,12 @@ Future _checkFrameworkArchs(String frameworkPath, String mode) async { throw TaskResult.failure('$mode $frameworkPath x86_64 architecture ${isDebug ? 'missing' : 'present'}'); } } + +Future _checkBitcode(String frameworkPath, String mode) async { + checkFileExists(frameworkPath); + + // Bitcode only needed in Release mode for archiving. + if (mode == 'Release' && !await containsBitcode(frameworkPath)) { + throw TaskResult.failure('$frameworkPath does not contain bitcode'); + } +} diff --git a/dev/devicelab/lib/framework/ios.dart b/dev/devicelab/lib/framework/ios.dart index 2d10b9bd010..2022c2c9807 100644 --- a/dev/devicelab/lib/framework/ios.dart +++ b/dev/devicelab/lib/framework/ios.dart @@ -54,6 +54,48 @@ Future dylibSymbols(String pathToDylib) { return eval('nm', ['-g', pathToDylib]); } -Future fileType(String pathToDylib) { - return eval('file', [pathToDylib]); +Future fileType(String pathToBinary) { + return eval('file', [pathToBinary]); +} + +Future containsBitcode(String pathToBinary) async { + // See: https://stackoverflow.com/questions/32755775/how-to-check-a-static-library-is-built-contain-bitcode + final String loadCommands = await eval('otool', [ + '-l', + pathToBinary, + ]); + if (!loadCommands.contains('__LLVM')) { + return false; + } + // Presence of the section may mean a bitcode marker was embedded (size=1), but there is no content. + if (!loadCommands.contains('size 0x0000000000000001')) { + return true; + } + // Check the false positives: size=1 wasn't referencing the __LLVM section. + + bool emptyBitcodeMarkerFound = false; + // Section + // sectname __bundle + // segname __LLVM + // addr 0x003c4000 + // size 0x0042b633 + // offset 3932160 + // ... + final List lines = LineSplitter.split(loadCommands).toList(); + 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( + (String line) => line.contains(' size 0x0000000000000001'), + orElse: () => null, + ); + if (emptyBitcodeMarker != null) { + emptyBitcodeMarkerFound = true; + return; + } + } + }); + return !emptyBitcodeMarkerFound; } diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart index f3358eebd68..b4f3598957b 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart @@ -159,7 +159,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { cache ??= globals.cache; for (final BuildMode mode in buildModes) { - globals.printStatus('Building framework for $iosProject in ${getNameForBuildMode(mode)} mode...'); + globals.printStatus('Building frameworks for $iosProject in ${getNameForBuildMode(mode)} mode...'); final String xcodeBuildConfiguration = toTitleCase(getNameForBuildMode(mode)); final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration); @@ -175,7 +175,7 @@ class BuildIOSFrameworkCommand extends BuildSubCommand { produceFlutterPodspec(mode, modeDirectory); } else { // Copy Flutter.framework. - await _produceFlutterFramework(outputDirectory, mode, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory); + await _produceFlutterFramework(mode, modeDirectory); } // Build aot, create module.framework and copy. @@ -266,10 +266,7 @@ end } Future _produceFlutterFramework( - Directory outputDirectory, BuildMode mode, - Directory iPhoneBuildOutput, - Directory simulatorBuildOutput, Directory modeDirectory, ) async { final Status status = globals.logger.startProgress( @@ -446,6 +443,15 @@ end final Status status = globals.logger.startProgress( ' ├─Building plugins...', timeout: timeoutConfiguration.slowOperation); try { + // Regardless of the last "flutter build" build mode, + // copy the corresponding engine. + // A plugin framework built with bitcode must link against the bitcode version + // of Flutter.framework (Release). + _project.ios.copyEngineArtifactToProject(mode); + + final String bitcodeGenerationMode = mode == BuildMode.release ? + 'bitcode' : 'marker'; // In release, force bitcode embedding without archiving. + List pluginsBuildCommand = [ 'xcrun', 'xcodebuild', @@ -455,6 +461,7 @@ end '-configuration', xcodeBuildConfiguration, 'SYMROOT=${iPhoneBuildOutput.path}', + 'BITCODE_GENERATION_MODE=$bitcodeGenerationMode', 'ONLY_ACTIVE_ARCH=NO' // No device targeted, so build all valid architectures. ]; @@ -592,7 +599,7 @@ end final Status status = globals.logger.startProgress( ' ├─Creating $frameworkBinaryName.xcframework...', - timeout: timeoutConfiguration.fastOperation, + timeout: timeoutConfiguration.slowOperation, ); try { if (mode == BuildMode.debug) { diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index f397a332b19..2340a7a851e 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -490,10 +490,6 @@ class IosProject extends FlutterProjectPlatform implements XcodeBasedProject { return; } - final Directory engineDest = ephemeralDirectory - .childDirectory('Flutter') - .childDirectory('engine'); - _deleteIfExistsSync(ephemeralDirectory); _overwriteFromTemplate( globals.fs.path.join('module', 'ios', 'library'), @@ -511,23 +507,32 @@ class IosProject extends FlutterProjectPlatform implements XcodeBasedProject { ephemeralDirectory, ); } - // Copy podspec and framework from engine cache. The actual build mode - // doesn't actually matter as it will be overwritten by xcode_backend.sh. - // However, cocoapods will run before that script and requires something - // to be in this location. - final Directory framework = globals.fs.directory( - globals.artifacts.getArtifactPath(Artifact.flutterFramework, + copyEngineArtifactToProject(BuildMode.debug); + } + } + + void copyEngineArtifactToProject(BuildMode mode) { + // Copy podspec and framework from engine cache. The actual build mode + // doesn't actually matter as it will be overwritten by xcode_backend.sh. + // However, cocoapods will run before that script and requires something + // to be in this location. + final Directory framework = globals.fs.directory( + globals.artifacts.getArtifactPath( + Artifact.flutterFramework, platform: TargetPlatform.ios, - mode: BuildMode.debug, - )); - if (framework.existsSync()) { - final File podspec = framework.parent.childFile('Flutter.podspec'); - fsUtils.copyDirectorySync( - framework, - engineDest.childDirectory('Flutter.framework'), - ); - podspec.copySync(engineDest.childFile('Flutter.podspec').path); - } + mode: mode, + ) + ); + if (framework.existsSync()) { + final Directory engineDest = ephemeralDirectory + .childDirectory('Flutter') + .childDirectory('engine'); + final File podspec = framework.parent.childFile('Flutter.podspec'); + fsUtils.copyDirectorySync( + framework, + engineDest.childDirectory('Flutter.framework'), + ); + podspec.copySync(engineDest.childFile('Flutter.podspec').path); } }