[iOS] Copy Flutter.framework.dSYM into app archive (#153215)

As of Xcode 16, App Store validation now requires that apps uploaded to the App store bundle dSYM debug information bundles for each Framework they embed.

dSYM bundles are packaged in the Flutter.xcframework shipped in the `ios-release` tools archive as of engine patches:
* https://github.com/flutter/engine/pull/54414
* https://github.com/flutter/engine/pull/54458

This copies the Flutter.framework.dSYM bundle from the tools cache to the app archive produced by `flutter build ipa`.

Issue: https://github.com/flutter/flutter/issues/116493
This commit is contained in:
Chris Bracken 2024-08-10 09:39:04 -07:00 committed by GitHub
parent 306e9e46f1
commit c375dd8d72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 159 additions and 12 deletions

View File

@ -22,6 +22,7 @@ enum Artifact {
/// The flutter tester binary.
flutterTester,
flutterFramework,
flutterFrameworkDsym,
flutterXcframework,
/// The framework directory of the macOS desktop.
flutterMacOSFramework,
@ -181,6 +182,8 @@ String? _artifactToFileName(Artifact artifact, Platform hostPlatform, [ BuildMod
return 'flutter_tester$exe';
case Artifact.flutterFramework:
return 'Flutter.framework';
case Artifact.flutterFrameworkDsym:
return 'Flutter.framework.dSYM';
case Artifact.flutterXcframework:
return 'Flutter.xcframework';
case Artifact.flutterMacOSFramework:
@ -623,6 +626,7 @@ class CachedArtifacts implements Artifacts {
case Artifact.frontendServerSnapshotForEngineDartSdk:
case Artifact.constFinder:
case Artifact.flutterFramework:
case Artifact.flutterFrameworkDsym:
case Artifact.flutterMacOSFramework:
case Artifact.flutterMacOSXcframework:
case Artifact.flutterPatchedSdkPath:
@ -656,7 +660,10 @@ class CachedArtifacts implements Artifacts {
return _fileSystem.path.join(engineDir, artifactFileName);
case Artifact.flutterFramework:
final String engineDir = _getEngineArtifactsPath(platform, mode)!;
return _getIosEngineArtifactPath(engineDir, environmentType, _fileSystem, _platform);
return _getIosFrameworkPath(engineDir, environmentType, _fileSystem, _platform);
case Artifact.flutterFrameworkDsym:
final String engineDir = _getEngineArtifactsPath(platform, mode)!;
return _getIosFrameworkDsymPath(engineDir, environmentType, _fileSystem, _platform);
case Artifact.engineDartSdkPath:
case Artifact.engineDartBinary:
case Artifact.engineDartAotRuntime:
@ -713,6 +720,7 @@ class CachedArtifacts implements Artifacts {
return _fileSystem.path.join(root, runtime, artifactFileName);
case Artifact.constFinder:
case Artifact.flutterFramework:
case Artifact.flutterFrameworkDsym:
case Artifact.flutterMacOSFramework:
case Artifact.flutterMacOSXcframework:
case Artifact.flutterTester:
@ -814,6 +822,7 @@ class CachedArtifacts implements Artifacts {
.childFile(_artifactToFileName(artifact, _platform, mode)!)
.path;
case Artifact.flutterFramework:
case Artifact.flutterFrameworkDsym:
case Artifact.flutterXcframework:
case Artifact.fuchsiaFlutterRunner:
case Artifact.fuchsiaKernelCompiler:
@ -882,8 +891,16 @@ TargetPlatform _currentHostPlatform(Platform platform, OperatingSystemUtils oper
throw UnimplementedError('Host OS not supported.');
}
String _getIosEngineArtifactPath(String engineDirectory,
EnvironmentType? environmentType, FileSystem fileSystem, Platform hostPlatform) {
/// Returns the Flutter.xcframework platform directory for the specified environment type.
///
/// `Flutter.xcframework` contains target environment/architecture-specific
/// subdirectories containing the appropriate `Flutter.framework` and
/// `dSYMs/Flutter.framework.dSYMs` bundles for that target architecture.
Directory _getIosFlutterFrameworkPlatformDirectory(
String engineDirectory,
EnvironmentType? environmentType,
FileSystem fileSystem,
Platform hostPlatform) {
final Directory xcframeworkDirectory = fileSystem
.directory(engineDirectory)
.childDirectory(_artifactToFileName(Artifact.flutterXcframework, hostPlatform)!);
@ -891,9 +908,7 @@ String _getIosEngineArtifactPath(String engineDirectory,
if (!xcframeworkDirectory.existsSync()) {
throwToolExit('No xcframework found at ${xcframeworkDirectory.path}. Try running "flutter precache --ios".');
}
Directory? flutterFrameworkSource;
for (final Directory platformDirectory
in xcframeworkDirectory.listSync().whereType<Directory>()) {
for (final Directory platformDirectory in xcframeworkDirectory.listSync().whereType<Directory>()) {
if (!platformDirectory.basename.startsWith('ios-')) {
continue;
}
@ -901,18 +916,47 @@ String _getIosEngineArtifactPath(String engineDirectory,
final bool simulatorDirectory = platformDirectory.basename.endsWith('-simulator');
if ((environmentType == EnvironmentType.simulator && simulatorDirectory) ||
(environmentType == EnvironmentType.physical && !simulatorDirectory)) {
flutterFrameworkSource = platformDirectory;
return platformDirectory;
}
}
if (flutterFrameworkSource == null) {
throwToolExit('No iOS frameworks found in ${xcframeworkDirectory.path}');
}
throwToolExit('No iOS frameworks found in ${xcframeworkDirectory.path}');
}
return flutterFrameworkSource
/// Returns the path to Flutter.framework.
String _getIosFrameworkPath(
String engineDirectory,
EnvironmentType? environmentType,
FileSystem fileSystem,
Platform hostPlatform) {
final Directory platformDir = _getIosFlutterFrameworkPlatformDirectory(
engineDirectory,
environmentType,
fileSystem,
hostPlatform,
);
return platformDir
.childDirectory(_artifactToFileName(Artifact.flutterFramework, hostPlatform)!)
.path;
}
/// Returns the path to Flutter.framework.dSYM.
String _getIosFrameworkDsymPath(
String engineDirectory,
EnvironmentType? environmentType,
FileSystem fileSystem,
Platform hostPlatform) {
final Directory platformDir = _getIosFlutterFrameworkPlatformDirectory(
engineDirectory,
environmentType,
fileSystem,
hostPlatform,
);
return platformDir
.childDirectory('dSYMs')
.childDirectory(_artifactToFileName(Artifact.flutterFrameworkDsym, hostPlatform)!)
.path;
}
String _getMacOSEngineArtifactPath(
String engineDirectory,
FileSystem fileSystem,
@ -1108,7 +1152,10 @@ class CachedLocalEngineArtifacts implements Artifacts {
case Artifact.platformLibrariesJson:
return _fileSystem.path.join(_getFlutterPatchedSdkPath(mode), 'lib', artifactFileName);
case Artifact.flutterFramework:
return _getIosEngineArtifactPath(
return _getIosFrameworkPath(
localEngineInfo.targetOutPath, environmentType, _fileSystem, _platform);
case Artifact.flutterFrameworkDsym:
return _getIosFrameworkDsymPath(
localEngineInfo.targetOutPath, environmentType, _fileSystem, _platform);
case Artifact.flutterMacOSFramework:
return _getMacOSEngineArtifactPath(
@ -1291,6 +1338,7 @@ class CachedLocalWebSdkArtifacts implements Artifacts {
case Artifact.genSnapshot:
case Artifact.flutterTester:
case Artifact.flutterFramework:
case Artifact.flutterFrameworkDsym:
case Artifact.flutterXcframework:
case Artifact.flutterMacOSFramework:
case Artifact.flutterMacOSXcframework:

View File

@ -300,6 +300,7 @@ abstract class UnpackIOS extends Target {
}
Future<void> _copyFramework(Environment environment, String sdkRoot) async {
// Copy Flutter framework.
final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem);
final String basePath = environment.artifacts.getArtifactPath(
Artifact.flutterFramework,
@ -324,6 +325,34 @@ abstract class UnpackIOS extends Target {
'${result.stdout}\n---\n${result.stderr}',
);
}
// Copy Flutter framework dSYM (debug symbol) bundle, if present.
final Directory frameworkDsym = environment.fileSystem.directory(
environment.artifacts.getArtifactPath(
Artifact.flutterFrameworkDsym,
platform: TargetPlatform.ios,
mode: buildMode,
environmentType: environmentType,
)
);
if (frameworkDsym.existsSync()) {
final ProcessResult result = await environment.processManager.run(<String>[
'rsync',
'-av',
'--delete',
'--filter',
'- .DS_Store/',
'--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r',
frameworkDsym.path,
environment.outputDir.path,
]);
if (result.exitCode != 0) {
throw Exception(
'Failed to copy framework dSYM (exit ${result.exitCode}:\n'
'${result.stdout}\n---\n${result.stderr}',
);
}
}
}
/// Destructively thin Flutter.framework to include only the specified architectures.

View File

@ -526,6 +526,8 @@ void main() {
late Directory outputDir;
late File binary;
late FakeCommand copyPhysicalFrameworkCommand;
late FakeCommand copyPhysicalFrameworkDsymCommand;
late FakeCommand copyPhysicalFrameworkDsymCommandFailure;
late FakeCommand lipoCommandNonFatResult;
late FakeCommand lipoVerifyArm64Command;
late FakeCommand xattrCommand;
@ -535,6 +537,7 @@ void main() {
final FileSystem fileSystem = MemoryFileSystem.test();
outputDir = fileSystem.directory('output');
binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter');
copyPhysicalFrameworkCommand = FakeCommand(command: <String>[
'rsync',
'-av',
@ -546,6 +549,28 @@ void main() {
outputDir.path,
]);
copyPhysicalFrameworkDsymCommand = FakeCommand(command: <String>[
'rsync',
'-av',
'--delete',
'--filter',
'- .DS_Store/',
'--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r',
'Artifact.flutterFrameworkDsym.TargetPlatform.ios.debug.EnvironmentType.physical',
outputDir.path,
]);
copyPhysicalFrameworkDsymCommandFailure = FakeCommand(command: <String>[
'rsync',
'-av',
'--delete',
'--filter',
'- .DS_Store/',
'--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r',
'Artifact.flutterFrameworkDsym.TargetPlatform.ios.debug.EnvironmentType.physical',
outputDir.path,
], exitCode: 1);
lipoCommandNonFatResult = FakeCommand(command: <String>[
'lipo',
'-info',
@ -643,6 +668,42 @@ void main() {
)));
});
testWithoutContext('fails when framework dSYM copy fails', () async {
binary.createSync(recursive: true);
final Directory dSYM = fileSystem.directory(
artifacts.getArtifactPath(Artifact.flutterFrameworkDsym,
platform: TargetPlatform.ios,
mode: BuildMode.debug,
environmentType: EnvironmentType.physical,
),
);
dSYM.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',
kSdkRoot: 'path/to/iPhoneOS.sdk',
},
);
processManager.addCommands(<FakeCommand>[
copyPhysicalFrameworkCommand,
copyPhysicalFrameworkDsymCommandFailure,
]);
await expectLater(
const DebugUnpackIOS().build(environment),
throwsA(isException.having(
(Exception exception) => exception.toString(),
'description',
contains('Failed to copy framework dSYM'),
)));
});
testWithoutContext('fails when requested archs missing from framework', () async {
binary.createSync(recursive: true);
@ -894,6 +955,14 @@ void main() {
testWithoutContext('codesigns framework', () async {
binary.createSync(recursive: true);
final Directory dSYM = fileSystem.directory(
artifacts.getArtifactPath(Artifact.flutterFrameworkDsym,
platform: TargetPlatform.ios,
mode: BuildMode.debug,
environmentType: EnvironmentType.physical,
),
);
dSYM.createSync(recursive: true);
final Environment environment = Environment.test(
fileSystem.currentDirectory,
@ -911,6 +980,7 @@ void main() {
processManager.addCommands(<FakeCommand>[
copyPhysicalFrameworkCommand,
copyPhysicalFrameworkDsymCommand,
lipoCommandNonFatResult,
lipoVerifyArm64Command,
xattrCommand,