diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh index 89044ea31d9..1f6ca64557a 100755 --- a/packages/flutter_tools/bin/xcode_backend.sh +++ b/packages/flutter_tools/bin/xcode_backend.sh @@ -204,73 +204,17 @@ is set to release or run \"flutter build ios --release\", then re-run Archive fr return 0 } -# Returns the CFBundleExecutable for the specified framework directory. -GetFrameworkExecutablePath() { - local framework_dir="$1" - - local plist_path="${framework_dir}/Info.plist" - local executable="$(defaults read "${plist_path}" CFBundleExecutable)" - echo "${framework_dir}/${executable}" -} - -# Destructively thins the specified executable file to include only the -# specified architectures. -LipoExecutable() { - local executable="$1" - shift - # Split $@ into an array. - read -r -a archs <<< "$@" - - # Extract architecture-specific framework executables. - local all_executables=() - for arch in "${archs[@]}"; do - local output="${executable}_${arch}" - local lipo_info="$(lipo -info "${executable}")" - if [[ "${lipo_info}" == "Non-fat file:"* ]]; then - if [[ "${lipo_info}" != *"${arch}" ]]; then - echo "Non-fat binary ${executable} is not ${arch}. Running lipo -info:" - echo "${lipo_info}" - exit 1 - fi - else - if lipo -output "${output}" -extract "${arch}" "${executable}"; then - all_executables+=("${output}") - else - echo "Failed to extract ${arch} for ${executable}. Running lipo -info:" - RunCommand lipo -info "${executable}" - exit 1 - fi - fi - done - - # Generate a merged binary from the architecture-specific executables. - # Skip this step for non-fat executables. - if [[ ${#all_executables[@]} > 0 ]]; then - local merged="${executable}_merged" - RunCommand lipo -output "${merged}" -create "${all_executables[@]}" - - RunCommand cp -f -- "${merged}" "${executable}" > /dev/null - RunCommand rm -f -- "${merged}" "${all_executables[@]}" - fi -} - -# Destructively thins the specified framework to include only the specified +# Destructively thins the Flutter and App frameworks to include only the specified # architectures. -ThinFramework() { - local framework_dir="$1" - shift - - local executable="$(GetFrameworkExecutablePath "${framework_dir}")" - LipoExecutable "${executable}" "$@" -} - ThinAppFrameworks() { - local xcode_frameworks_dir="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" - - [[ -d "${xcode_frameworks_dir}" ]] || return 0 - find "${xcode_frameworks_dir}" -type d -name "*.framework" | while read framework_dir; do - ThinFramework "$framework_dir" "$ARCHS" - done + RunCommand "${FLUTTER_ROOT}/bin/flutter" \ + ${verbose_flag} \ + assemble \ + --no-version-check \ + --output="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" \ + -dTargetPlatform=ios \ + -dIosArchs="${ARCHS}" \ + "thin_ios_application_frameworks" } # Adds the App.framework as an embedded binary and the flutter_assets as 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 61d71be37a6..3375d381052 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/ios.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/ios.dart @@ -417,7 +417,6 @@ abstract class IosAssetBundle extends Target { environment.buildDir.childFile('flutter_assets.d'), ); - // Copy the plist from either the project or module. // TODO(jonahwilliams): add plist to inputs final FlutterProject flutterProject = FlutterProject.fromDirectory(environment.projectDir); @@ -535,3 +534,84 @@ Future createStubAppFramework(File outputFile, String sdkRoot, } } } + +/// Destructively thins the specified executable file to include only the specified architectures. +/// +/// This target is not fingerprinted and will always run. +class ThinIosApplicationFrameworks extends Target { + const ThinIosApplicationFrameworks(); + + @override + String get name => 'thin_ios_application_frameworks'; + + @override + List get dependencies => const []; + + @override + List get inputs => const []; + + @override + List get outputs => const []; + + @override + Future build(Environment environment) async { + if (environment.defines[kIosArchs] == null) { + throw MissingDefineException(kIosArchs, 'thin_ios_application_frameworks'); + } + final Directory frameworkDirectory = environment.outputDir; + + final File appFramework = frameworkDirectory.childDirectory('App.framework').childFile('App'); + final File flutterFramework = frameworkDirectory.childDirectory('Flutter.framework').childFile('Flutter'); + await _thinBinary(appFramework, environment); + await _thinBinary(flutterFramework, environment); + } + + Future _thinBinary(File binary, Environment environment) async { + final String binaryPath = binary.path; + if (!binary.existsSync()) { + throw Exception('Binary $binaryPath does not exist, cannot thin'); + } + final String archs = environment.defines[kIosArchs]; + final List archList = archs.split(' ').toList(); + final ProcessResult infoResult = environment.processManager.runSync([ + 'lipo', + '-info', + binaryPath, + ]); + final String lipoInfo = infoResult.stdout as String; + + final ProcessResult verifyResult = environment.processManager.runSync([ + 'lipo', + binaryPath, + '-verify_arch', + ...archList + ]); + + if (verifyResult.exitCode != 0) { + throw Exception('Binary $binaryPath does not contain $archs. Running lipo -info:\n$lipoInfo'); + } + + // Skip this step for non-fat executables. + if (lipoInfo.startsWith('Non-fat file:')) { + environment.logger.printTrace('Skipping lipo for non-fat file $binaryPath'); + return; + } + + // Thin in-place. + final ProcessResult extractResult = environment.processManager.runSync([ + 'lipo', + '-output', + binaryPath, + for (final String arch in archList) + ...[ + '-extract', + arch, + ], + ...[binaryPath], + ]); + + if (extractResult.exitCode != 0) { + throw Exception('Failed to extract $archs for $binaryPath.\n${extractResult.stderr}\nRunning lipo -info:\n$lipoInfo'); + } + } +} diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 5471387bee1..decbe99b429 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -66,6 +66,7 @@ const List _kDefaultTargets = [ DebugIosApplicationBundle(), ProfileIosApplicationBundle(), ReleaseIosApplicationBundle(), + ThinIosApplicationFrameworks(), // Windows targets UnpackWindows(), DebugBundleWindowsAssets(), 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 3a7b524c779..ca474e7227f 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 @@ -45,7 +45,7 @@ void main() { FileSystem fileSystem; FakeProcessManager processManager; Artifacts artifacts; - Logger logger; + BufferLogger logger; setUp(() { fileSystem = MemoryFileSystem.test(); @@ -300,4 +300,274 @@ void main() { await const DebugUnpackIOS().build(environment); }); }); + + group('thin frameworks', () { + testWithoutContext('fails when frameworks missing', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory outputDir = fileSystem.directory('Runner.app').childDirectory('Frameworks'); + final Environment environment = Environment.test( + fileSystem.currentDirectory, + processManager: processManager, + artifacts: artifacts, + logger: logger, + fileSystem: fileSystem, + outputDir: outputDir, + defines: { + kIosArchs: 'arm64', + }, + ); + expect( + const ThinIosApplicationFrameworks().build(environment), + throwsA(isA().having( + (Exception exception) => exception.toString(), + 'description', + contains('App.framework/App does not exist, cannot thin'), + ))); + }); + + testWithoutContext('fails when requested archs missing from framework', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory outputDir = fileSystem.directory('Runner.app').childDirectory('Frameworks')..createSync(recursive: true); + final File appBinary = outputDir.childDirectory('App.framework').childFile('App')..createSync(recursive: true); + + final Environment environment = Environment.test( + fileSystem.currentDirectory, + processManager: processManager, + artifacts: artifacts, + logger: logger, + fileSystem: fileSystem, + outputDir: outputDir, + defines: { + kIosArchs: 'arm64 armv7', + }, + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-info', + appBinary.path, + ], stdout: 'Architectures in the fat file:'), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + appBinary.path, + '-verify_arch', + 'arm64', + 'armv7', + ], exitCode: 1), + ); + + expect( + const ThinIosApplicationFrameworks().build(environment), + throwsA(isA().having( + (Exception exception) => exception.toString(), + 'description', + contains('does not contain arm64 armv7. Running lipo -info:\nArchitectures in the fat file:'), + ))); + }); + + testWithoutContext('fails when lipo extract fails', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory outputDir = fileSystem.directory('Runner.app').childDirectory('Frameworks')..createSync(recursive: true); + final File appBinary = outputDir.childDirectory('App.framework').childFile('App')..createSync(recursive: true); + + final Environment environment = Environment.test( + fileSystem.currentDirectory, + processManager: processManager, + artifacts: artifacts, + logger: logger, + fileSystem: fileSystem, + outputDir: outputDir, + defines: { + kIosArchs: 'arm64 armv7', + }, + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-info', + appBinary.path, + ], stdout: 'Architectures in the fat file:'), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + appBinary.path, + '-verify_arch', + 'arm64', + 'armv7', + ]), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-output', + appBinary.path, + '-extract', + 'arm64', + '-extract', + 'armv7', + appBinary.path, + ], exitCode: 1, + stderr: 'lipo error'), + ); + + expect( + const ThinIosApplicationFrameworks().build(environment), + throwsA(isA().having( + (Exception exception) => exception.toString(), + 'description', + contains('Failed to extract arm64 armv7 for Runner.app/Frameworks/App.framework/App.\nlipo error\nRunning lipo -info:\nArchitectures in the fat file:'), + ))); + }); + + testWithoutContext('skips thin frameworks', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory outputDir = fileSystem.directory('Runner.app').childDirectory('Frameworks')..createSync(recursive: true); + final File appBinary = outputDir.childDirectory('App.framework').childFile('App')..createSync(recursive: true); + final File flutterBinary = 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', + }, + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-info', + appBinary.path, + ], stdout: 'Non-fat file:'), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + appBinary.path, + '-verify_arch', + 'arm64', + ]), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-info', + flutterBinary.path, + ], stdout: 'Non-fat file:'), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + flutterBinary.path, + '-verify_arch', + 'arm64', + ]), + ); + await const ThinIosApplicationFrameworks().build(environment); + + expect(logger.traceText, contains('Skipping lipo for non-fat file Runner.app/Frameworks/App.framework/App')); + expect(logger.traceText, contains('Skipping lipo for non-fat file Runner.app/Frameworks/Flutter.framework/Flutter')); + + expect(processManager.hasRemainingExpectations, isFalse); + }); + + testWithoutContext('thins fat frameworks', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final Directory outputDir = fileSystem.directory('Runner.app').childDirectory('Frameworks')..createSync(recursive: true); + final File appBinary = outputDir.childDirectory('App.framework').childFile('App')..createSync(recursive: true); + final File flutterBinary = 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 armv7', + }, + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-info', + appBinary.path, + ], stdout: 'Architectures in the fat file:'), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + appBinary.path, + '-verify_arch', + 'arm64', + 'armv7', + ]), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-output', + appBinary.path, + '-extract', + 'arm64', + '-extract', + 'armv7', + appBinary.path, + ]), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-info', + flutterBinary.path, + ], stdout: 'Architectures in the fat file:'), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + flutterBinary.path, + '-verify_arch', + 'arm64', + 'armv7', + ]), + ); + + processManager.addCommand( + FakeCommand(command: [ + 'lipo', + '-output', + flutterBinary.path, + '-extract', + 'arm64', + '-extract', + 'armv7', + flutterBinary.path, + ]), + ); + + await const ThinIosApplicationFrameworks().build(environment); + expect(processManager.hasRemainingExpectations, isFalse); + }); + }); } 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 737add31727..2cc46a26fa3 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 @@ -195,6 +195,8 @@ void main() { 'VERBOSE_SCRIPT_LOGGING': '1', 'FLUTTER_BUILD_MODE': 'release', 'ACTION': 'install', + 'ARCHS': 'arm64 armv7', + 'FLUTTER_ROOT': flutterRoot, // Skip bitcode stripping since we just checked that above. }, );