Unverified Commit 5bc100da authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Move iOS framework thinning into the tool (#76665)

parent 9df362a7
......@@ -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
......
......@@ -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<RunResult> 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<Target> get dependencies => const <Target>[];
@override
List<Source> get inputs => const <Source>[];
@override
List<Source> get outputs => const <Source>[];
@override
Future<void> 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<void> _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<String> archList = archs.split(' ').toList();
final ProcessResult infoResult = environment.processManager.runSync(<String>[
'lipo',
'-info',
binaryPath,
]);
final String lipoInfo = infoResult.stdout as String;
final ProcessResult verifyResult = environment.processManager.runSync(<String>[
'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(<String>[
'lipo',
'-output',
binaryPath,
for (final String arch in archList)
...<String>[
'-extract',
arch,
],
...<String>[binaryPath],
]);
if (extractResult.exitCode != 0) {
throw Exception('Failed to extract $archs for $binaryPath.\n${extractResult.stderr}\nRunning lipo -info:\n$lipoInfo');
}
}
}
......@@ -66,6 +66,7 @@ const List<Target> _kDefaultTargets = <Target>[
DebugIosApplicationBundle(),
ProfileIosApplicationBundle(),
ReleaseIosApplicationBundle(),
ThinIosApplicationFrameworks(),
// Windows targets
UnpackWindows(),
DebugBundleWindowsAssets(),
......
......@@ -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: <String, String>{
kIosArchs: 'arm64',
},
);
expect(
const ThinIosApplicationFrameworks().build(environment),
throwsA(isA<Exception>().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: <String, String>{
kIosArchs: 'arm64 armv7',
},
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-info',
appBinary.path,
], stdout: 'Architectures in the fat file:'),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
appBinary.path,
'-verify_arch',
'arm64',
'armv7',
], exitCode: 1),
);
expect(
const ThinIosApplicationFrameworks().build(environment),
throwsA(isA<Exception>().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: <String, String>{
kIosArchs: 'arm64 armv7',
},
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-info',
appBinary.path,
], stdout: 'Architectures in the fat file:'),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
appBinary.path,
'-verify_arch',
'arm64',
'armv7',
]),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-output',
appBinary.path,
'-extract',
'arm64',
'-extract',
'armv7',
appBinary.path,
], exitCode: 1,
stderr: 'lipo error'),
);
expect(
const ThinIosApplicationFrameworks().build(environment),
throwsA(isA<Exception>().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: <String, String>{
kIosArchs: 'arm64',
},
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-info',
appBinary.path,
], stdout: 'Non-fat file:'),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
appBinary.path,
'-verify_arch',
'arm64',
]),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-info',
flutterBinary.path,
], stdout: 'Non-fat file:'),
);
processManager.addCommand(
FakeCommand(command: <String>[
'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: <String, String>{
kIosArchs: 'arm64 armv7',
},
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-info',
appBinary.path,
], stdout: 'Architectures in the fat file:'),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
appBinary.path,
'-verify_arch',
'arm64',
'armv7',
]),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-output',
appBinary.path,
'-extract',
'arm64',
'-extract',
'armv7',
appBinary.path,
]),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-info',
flutterBinary.path,
], stdout: 'Architectures in the fat file:'),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
flutterBinary.path,
'-verify_arch',
'arm64',
'armv7',
]),
);
processManager.addCommand(
FakeCommand(command: <String>[
'lipo',
'-output',
flutterBinary.path,
'-extract',
'arm64',
'-extract',
'armv7',
flutterBinary.path,
]),
);
await const ThinIosApplicationFrameworks().build(environment);
expect(processManager.hasRemainingExpectations, isFalse);
});
});
}
......@@ -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.
},
);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment