Skip to content

Commit 723b82e

Browse files
authored
Feat: dSYM debug info for iOS & macOS builds (flutter#101586)
1 parent 96345a4 commit 723b82e

File tree

15 files changed

+602
-82
lines changed

15 files changed

+602
-82
lines changed

dev/devicelab/bin/tasks/build_ios_framework_module_test.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,23 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
189189
'vm_snapshot_data',
190190
));
191191

192+
final String appFrameworkDsymPath = path.join(
193+
outputPath,
194+
mode,
195+
'App.xcframework',
196+
'ios-arm64',
197+
'dSYMs',
198+
'App.framework.dSYM'
199+
);
200+
checkDirectoryExists(appFrameworkDsymPath);
201+
await _checkDsym(path.join(
202+
appFrameworkDsymPath,
203+
'Contents',
204+
'Resources',
205+
'DWARF',
206+
'App',
207+
));
208+
192209
checkFileExists(path.join(
193210
outputPath,
194211
mode,
@@ -404,6 +421,25 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
404421
'App',
405422
));
406423

424+
if (mode != 'Debug') {
425+
final String appFrameworkDsymPath = path.join(
426+
cocoapodsOutputPath,
427+
mode,
428+
'App.xcframework',
429+
'ios-arm64',
430+
'dSYMs',
431+
'App.framework.dSYM'
432+
);
433+
checkDirectoryExists(appFrameworkDsymPath);
434+
await _checkDsym(path.join(
435+
appFrameworkDsymPath,
436+
'Contents',
437+
'Resources',
438+
'DWARF',
439+
'App',
440+
));
441+
}
442+
407443
if (Directory(path.join(
408444
cocoapodsOutputPath,
409445
mode,
@@ -582,6 +618,23 @@ Future<void> _testBuildMacOSFramework(Directory projectDir) async {
582618
'Resources',
583619
'Info.plist',
584620
));
621+
622+
final String appFrameworkDsymPath = path.join(
623+
outputPath,
624+
mode,
625+
'App.xcframework',
626+
'macos-arm64_x86_64',
627+
'dSYMs',
628+
'App.framework.dSYM'
629+
);
630+
checkDirectoryExists(appFrameworkDsymPath);
631+
await _checkDsym(path.join(
632+
appFrameworkDsymPath,
633+
'Contents',
634+
'Resources',
635+
'DWARF',
636+
'App',
637+
));
585638
}
586639

587640
section("Check all modes' engine dylib");
@@ -712,6 +765,25 @@ Future<void> _testBuildMacOSFramework(Directory projectDir) async {
712765
'App',
713766
));
714767

768+
if (mode != 'Debug') {
769+
final String appFrameworkDsymPath = path.join(
770+
cocoapodsOutputPath,
771+
mode,
772+
'App.xcframework',
773+
'macos-arm64_x86_64',
774+
'dSYMs',
775+
'App.framework.dSYM'
776+
);
777+
checkDirectoryExists(appFrameworkDsymPath);
778+
await _checkDsym(path.join(
779+
appFrameworkDsymPath,
780+
'Contents',
781+
'Resources',
782+
'DWARF',
783+
'App',
784+
));
785+
}
786+
715787
await _checkStatic(path.join(
716788
cocoapodsOutputPath,
717789
mode,
@@ -750,6 +822,13 @@ Future<void> _checkDylib(String pathToLibrary) async {
750822
}
751823
}
752824

825+
Future<void> _checkDsym(String pathToSymbolFile) async {
826+
final String binaryFileType = await fileType(pathToSymbolFile);
827+
if (!binaryFileType.contains('dSYM companion file')) {
828+
throw TaskResult.failure('$pathToSymbolFile is not a dSYM, found: $binaryFileType');
829+
}
830+
}
831+
753832
Future<void> _checkStatic(String pathToLibrary) async {
754833
final String binaryFileType = await fileType(pathToLibrary);
755834
if (!binaryFileType.contains('current ar archive random library')) {

dev/devicelab/bin/tasks/module_test_ios.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,16 @@ end
440440
'Frameworks',
441441
'url_launcher_ios.framework',
442442
));
443+
444+
checkFileExists(path.join(
445+
'${objectiveCBuildArchiveDirectory.path}.xcarchive',
446+
'dSYMs',
447+
'App.framework.dSYM',
448+
'Contents',
449+
'Resources',
450+
'DWARF',
451+
'App'
452+
));
443453
});
444454

445455
section('Run platform unit tests');

packages/flutter_tools/lib/src/base/build.dart

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,17 @@ class AOTSnapshotter {
139139
'--deterministic',
140140
];
141141

142+
final bool targetingApplePlatform =
143+
platform == TargetPlatform.ios || platform == TargetPlatform.darwin;
144+
_logger.printTrace('targetingApplePlatform = $targetingApplePlatform');
145+
146+
final bool extractAppleDebugSymbols =
147+
buildMode == BuildMode.profile || buildMode == BuildMode.release;
148+
_logger.printTrace('extractAppleDebugSymbols = $extractAppleDebugSymbols');
149+
142150
// We strip snapshot by default, but allow to suppress this behavior
143151
// by supplying --no-strip in extraGenSnapshotOptions.
144152
bool shouldStrip = true;
145-
146153
if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
147154
_logger.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
148155
for (final String option in extraGenSnapshotOptions) {
@@ -168,8 +175,20 @@ class AOTSnapshotter {
168175
]);
169176
}
170177

171-
if (shouldStrip) {
172-
genSnapshotArgs.add('--strip');
178+
// When buiding for iOS and splitting out debug info, we want to strip
179+
// manually after the dSYM export, instead of in the `gen_snapshot`.
180+
final bool stripAfterBuild;
181+
if (targetingApplePlatform) {
182+
stripAfterBuild = shouldStrip;
183+
if (stripAfterBuild) {
184+
_logger.printTrace('Will strip AOT snapshot manual after build and dSYM generation.');
185+
}
186+
} else {
187+
stripAfterBuild = false;
188+
if (shouldStrip) {
189+
genSnapshotArgs.add('--strip');
190+
_logger.printTrace('Will strip AOT snapshot during build.');
191+
}
173192
}
174193

175194
if (platform == TargetPlatform.android_arm) {
@@ -218,33 +237,35 @@ class AOTSnapshotter {
218237

219238
// On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the
220239
// end-developer can link into their app.
221-
if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin) {
222-
final RunResult result = await _buildFramework(
240+
if (targetingApplePlatform) {
241+
return _buildFramework(
223242
appleArch: darwinArch!,
224243
isIOS: platform == TargetPlatform.ios,
225244
sdkRoot: sdkRoot,
226245
assemblyPath: assembly,
227246
outputPath: outputDir.path,
228247
bitcode: bitcode,
229248
quiet: quiet,
249+
stripAfterBuild: stripAfterBuild,
250+
extractAppleDebugSymbols: extractAppleDebugSymbols
230251
);
231-
if (result.exitCode != 0) {
232-
return result.exitCode;
233-
}
252+
} else {
253+
return 0;
234254
}
235-
return 0;
236255
}
237256

238257
/// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly
239258
/// source at [assemblyPath].
240-
Future<RunResult> _buildFramework({
259+
Future<int> _buildFramework({
241260
required DarwinArch appleArch,
242261
required bool isIOS,
243262
String? sdkRoot,
244263
required String assemblyPath,
245264
required String outputPath,
246265
required bool bitcode,
247-
required bool quiet
266+
required bool quiet,
267+
required bool stripAfterBuild,
268+
required bool extractAppleDebugSymbols
248269
}) async {
249270
final String targetArch = getNameForDarwinArch(appleArch);
250271
if (!quiet) {
@@ -278,7 +299,7 @@ class AOTSnapshotter {
278299
]);
279300
if (compileResult.exitCode != 0) {
280301
_logger.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
281-
return compileResult;
302+
return compileResult.exitCode;
282303
}
283304

284305
final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework');
@@ -294,11 +315,33 @@ class AOTSnapshotter {
294315
'-o', appLib,
295316
assemblyO,
296317
];
318+
297319
final RunResult linkResult = await _xcode.clang(linkArgs);
298320
if (linkResult.exitCode != 0) {
299-
_logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
321+
_logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${linkResult.exitCode}');
322+
return linkResult.exitCode;
323+
}
324+
325+
if (extractAppleDebugSymbols) {
326+
final RunResult dsymResult = await _xcode.dsymutil(<String>['-o', '$frameworkDir.dSYM', appLib]);
327+
if (dsymResult.exitCode != 0) {
328+
_logger.printError('Failed to generate dSYM - dsymutil terminated with exit code ${dsymResult.exitCode}');
329+
return dsymResult.exitCode;
330+
}
331+
332+
if (stripAfterBuild) {
333+
// See https://www.unix.com/man-page/osx/1/strip/ for arguments
334+
final RunResult stripResult = await _xcode.strip(<String>['-S', appLib, '-o', appLib]);
335+
if (stripResult.exitCode != 0) {
336+
_logger.printError('Failed to strip debugging symbols from the generated AOT snapshot - strip terminated with exit code ${stripResult.exitCode}');
337+
return stripResult.exitCode;
338+
}
339+
}
340+
} else {
341+
assert(stripAfterBuild == false);
300342
}
301-
return linkResult;
343+
344+
return 0;
302345
}
303346

304347
bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {

packages/flutter_tools/lib/src/build_system/targets/common.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:package_config/package_config.dart';
77
import '../../artifacts.dart';
88
import '../../base/build.dart';
99
import '../../base/file_system.dart';
10+
import '../../base/io.dart';
1011
import '../../build_info.dart';
1112
import '../../compile.dart';
1213
import '../../dart/package_map.dart';
@@ -394,3 +395,48 @@ abstract class CopyFlutterAotBundle extends Target {
394395
environment.buildDir.childFile('app.so').copySync(outputFile.path);
395396
}
396397
}
398+
399+
/// Lipo CLI tool wrapper shared by iOS and macOS builds.
400+
class Lipo {
401+
/// Static only.
402+
Lipo._();
403+
404+
/// Create a "fat" binary by combining multiple architecture-specific ones.
405+
/// `skipMissingInputs` can be changed to `true` to first check whether
406+
/// the expected input paths exist and ignore the command if they don't.
407+
/// Otherwise, `lipo` would fail if the given paths didn't exist.
408+
static Future<void> create(
409+
Environment environment,
410+
List<DarwinArch> darwinArchs, {
411+
required String relativePath,
412+
required String inputDir,
413+
bool skipMissingInputs = false,
414+
}) async {
415+
416+
final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, relativePath);
417+
environment.fileSystem.directory(resultPath).parent.createSync(recursive: true);
418+
419+
Iterable<String> inputPaths = darwinArchs.map(
420+
(DarwinArch iosArch) => environment.fileSystem.path.join(inputDir, getNameForDarwinArch(iosArch), relativePath)
421+
);
422+
if (skipMissingInputs) {
423+
inputPaths = inputPaths.where(environment.fileSystem.isFileSync);
424+
if (inputPaths.isEmpty) {
425+
return;
426+
}
427+
}
428+
429+
final List<String> lipoArgs = <String>[
430+
'lipo',
431+
...inputPaths,
432+
'-create',
433+
'-output',
434+
resultPath,
435+
];
436+
437+
final ProcessResult result = await environment.processManager.run(lipoArgs);
438+
if (result.exitCode != 0) {
439+
throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}');
440+
}
441+
}
442+
}

0 commit comments

Comments
 (0)