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

View File

@ -300,6 +300,7 @@ abstract class UnpackIOS extends Target {
} }
Future<void> _copyFramework(Environment environment, String sdkRoot) async { Future<void> _copyFramework(Environment environment, String sdkRoot) async {
// Copy Flutter framework.
final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem); final EnvironmentType? environmentType = environmentTypeFromSdkroot(sdkRoot, environment.fileSystem);
final String basePath = environment.artifacts.getArtifactPath( final String basePath = environment.artifacts.getArtifactPath(
Artifact.flutterFramework, Artifact.flutterFramework,
@ -324,6 +325,34 @@ abstract class UnpackIOS extends Target {
'${result.stdout}\n---\n${result.stderr}', '${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. /// Destructively thin Flutter.framework to include only the specified architectures.

View File

@ -526,6 +526,8 @@ void main() {
late Directory outputDir; late Directory outputDir;
late File binary; late File binary;
late FakeCommand copyPhysicalFrameworkCommand; late FakeCommand copyPhysicalFrameworkCommand;
late FakeCommand copyPhysicalFrameworkDsymCommand;
late FakeCommand copyPhysicalFrameworkDsymCommandFailure;
late FakeCommand lipoCommandNonFatResult; late FakeCommand lipoCommandNonFatResult;
late FakeCommand lipoVerifyArm64Command; late FakeCommand lipoVerifyArm64Command;
late FakeCommand xattrCommand; late FakeCommand xattrCommand;
@ -535,6 +537,7 @@ void main() {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
outputDir = fileSystem.directory('output'); outputDir = fileSystem.directory('output');
binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter'); binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter');
copyPhysicalFrameworkCommand = FakeCommand(command: <String>[ copyPhysicalFrameworkCommand = FakeCommand(command: <String>[
'rsync', 'rsync',
'-av', '-av',
@ -546,6 +549,28 @@ void main() {
outputDir.path, 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>[ lipoCommandNonFatResult = FakeCommand(command: <String>[
'lipo', 'lipo',
'-info', '-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 { testWithoutContext('fails when requested archs missing from framework', () async {
binary.createSync(recursive: true); binary.createSync(recursive: true);
@ -894,6 +955,14 @@ void main() {
testWithoutContext('codesigns framework', () async { testWithoutContext('codesigns framework', () async {
binary.createSync(recursive: true); 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( final Environment environment = Environment.test(
fileSystem.currentDirectory, fileSystem.currentDirectory,
@ -911,6 +980,7 @@ void main() {
processManager.addCommands(<FakeCommand>[ processManager.addCommands(<FakeCommand>[
copyPhysicalFrameworkCommand, copyPhysicalFrameworkCommand,
copyPhysicalFrameworkDsymCommand,
lipoCommandNonFatResult, lipoCommandNonFatResult,
lipoVerifyArm64Command, lipoVerifyArm64Command,
xattrCommand, xattrCommand,