Unverified Commit 723b82e4 authored by Ivan Dlugos's avatar Ivan Dlugos Committed by GitHub

Feat: dSYM debug info for iOS & macOS builds (#101586)

parent 96345a4b
......@@ -189,6 +189,23 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
'vm_snapshot_data',
));
final String appFrameworkDsymPath = path.join(
outputPath,
mode,
'App.xcframework',
'ios-arm64',
'dSYMs',
'App.framework.dSYM'
);
checkDirectoryExists(appFrameworkDsymPath);
await _checkDsym(path.join(
appFrameworkDsymPath,
'Contents',
'Resources',
'DWARF',
'App',
));
checkFileExists(path.join(
outputPath,
mode,
......@@ -404,6 +421,25 @@ Future<void> _testBuildIosFramework(Directory projectDir, { bool isModule = fals
'App',
));
if (mode != 'Debug') {
final String appFrameworkDsymPath = path.join(
cocoapodsOutputPath,
mode,
'App.xcframework',
'ios-arm64',
'dSYMs',
'App.framework.dSYM'
);
checkDirectoryExists(appFrameworkDsymPath);
await _checkDsym(path.join(
appFrameworkDsymPath,
'Contents',
'Resources',
'DWARF',
'App',
));
}
if (Directory(path.join(
cocoapodsOutputPath,
mode,
......@@ -582,6 +618,23 @@ Future<void> _testBuildMacOSFramework(Directory projectDir) async {
'Resources',
'Info.plist',
));
final String appFrameworkDsymPath = path.join(
outputPath,
mode,
'App.xcframework',
'macos-arm64_x86_64',
'dSYMs',
'App.framework.dSYM'
);
checkDirectoryExists(appFrameworkDsymPath);
await _checkDsym(path.join(
appFrameworkDsymPath,
'Contents',
'Resources',
'DWARF',
'App',
));
}
section("Check all modes' engine dylib");
......@@ -712,6 +765,25 @@ Future<void> _testBuildMacOSFramework(Directory projectDir) async {
'App',
));
if (mode != 'Debug') {
final String appFrameworkDsymPath = path.join(
cocoapodsOutputPath,
mode,
'App.xcframework',
'macos-arm64_x86_64',
'dSYMs',
'App.framework.dSYM'
);
checkDirectoryExists(appFrameworkDsymPath);
await _checkDsym(path.join(
appFrameworkDsymPath,
'Contents',
'Resources',
'DWARF',
'App',
));
}
await _checkStatic(path.join(
cocoapodsOutputPath,
mode,
......@@ -750,6 +822,13 @@ Future<void> _checkDylib(String pathToLibrary) async {
}
}
Future<void> _checkDsym(String pathToSymbolFile) async {
final String binaryFileType = await fileType(pathToSymbolFile);
if (!binaryFileType.contains('dSYM companion file')) {
throw TaskResult.failure('$pathToSymbolFile is not a dSYM, found: $binaryFileType');
}
}
Future<void> _checkStatic(String pathToLibrary) async {
final String binaryFileType = await fileType(pathToLibrary);
if (!binaryFileType.contains('current ar archive random library')) {
......
......@@ -440,6 +440,16 @@ end
'Frameworks',
'url_launcher_ios.framework',
));
checkFileExists(path.join(
'${objectiveCBuildArchiveDirectory.path}.xcarchive',
'dSYMs',
'App.framework.dSYM',
'Contents',
'Resources',
'DWARF',
'App'
));
});
section('Run platform unit tests');
......
......@@ -139,10 +139,17 @@ class AOTSnapshotter {
'--deterministic',
];
final bool targetingApplePlatform =
platform == TargetPlatform.ios || platform == TargetPlatform.darwin;
_logger.printTrace('targetingApplePlatform = $targetingApplePlatform');
final bool extractAppleDebugSymbols =
buildMode == BuildMode.profile || buildMode == BuildMode.release;
_logger.printTrace('extractAppleDebugSymbols = $extractAppleDebugSymbols');
// We strip snapshot by default, but allow to suppress this behavior
// by supplying --no-strip in extraGenSnapshotOptions.
bool shouldStrip = true;
if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
_logger.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
for (final String option in extraGenSnapshotOptions) {
......@@ -168,8 +175,20 @@ class AOTSnapshotter {
]);
}
if (shouldStrip) {
genSnapshotArgs.add('--strip');
// When buiding for iOS and splitting out debug info, we want to strip
// manually after the dSYM export, instead of in the `gen_snapshot`.
final bool stripAfterBuild;
if (targetingApplePlatform) {
stripAfterBuild = shouldStrip;
if (stripAfterBuild) {
_logger.printTrace('Will strip AOT snapshot manual after build and dSYM generation.');
}
} else {
stripAfterBuild = false;
if (shouldStrip) {
genSnapshotArgs.add('--strip');
_logger.printTrace('Will strip AOT snapshot during build.');
}
}
if (platform == TargetPlatform.android_arm) {
......@@ -218,8 +237,8 @@ class AOTSnapshotter {
// On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the
// end-developer can link into their app.
if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin) {
final RunResult result = await _buildFramework(
if (targetingApplePlatform) {
return _buildFramework(
appleArch: darwinArch!,
isIOS: platform == TargetPlatform.ios,
sdkRoot: sdkRoot,
......@@ -227,24 +246,26 @@ class AOTSnapshotter {
outputPath: outputDir.path,
bitcode: bitcode,
quiet: quiet,
stripAfterBuild: stripAfterBuild,
extractAppleDebugSymbols: extractAppleDebugSymbols
);
if (result.exitCode != 0) {
return result.exitCode;
}
} else {
return 0;
}
return 0;
}
/// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly
/// source at [assemblyPath].
Future<RunResult> _buildFramework({
Future<int> _buildFramework({
required DarwinArch appleArch,
required bool isIOS,
String? sdkRoot,
required String assemblyPath,
required String outputPath,
required bool bitcode,
required bool quiet
required bool quiet,
required bool stripAfterBuild,
required bool extractAppleDebugSymbols
}) async {
final String targetArch = getNameForDarwinArch(appleArch);
if (!quiet) {
......@@ -278,7 +299,7 @@ class AOTSnapshotter {
]);
if (compileResult.exitCode != 0) {
_logger.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
return compileResult;
return compileResult.exitCode;
}
final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework');
......@@ -294,11 +315,33 @@ class AOTSnapshotter {
'-o', appLib,
assemblyO,
];
final RunResult linkResult = await _xcode.clang(linkArgs);
if (linkResult.exitCode != 0) {
_logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
_logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${linkResult.exitCode}');
return linkResult.exitCode;
}
if (extractAppleDebugSymbols) {
final RunResult dsymResult = await _xcode.dsymutil(<String>['-o', '$frameworkDir.dSYM', appLib]);
if (dsymResult.exitCode != 0) {
_logger.printError('Failed to generate dSYM - dsymutil terminated with exit code ${dsymResult.exitCode}');
return dsymResult.exitCode;
}
if (stripAfterBuild) {
// See https://www.unix.com/man-page/osx/1/strip/ for arguments
final RunResult stripResult = await _xcode.strip(<String>['-S', appLib, '-o', appLib]);
if (stripResult.exitCode != 0) {
_logger.printError('Failed to strip debugging symbols from the generated AOT snapshot - strip terminated with exit code ${stripResult.exitCode}');
return stripResult.exitCode;
}
}
} else {
assert(stripAfterBuild == false);
}
return linkResult;
return 0;
}
bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
......
......@@ -7,6 +7,7 @@ import 'package:package_config/package_config.dart';
import '../../artifacts.dart';
import '../../base/build.dart';
import '../../base/file_system.dart';
import '../../base/io.dart';
import '../../build_info.dart';
import '../../compile.dart';
import '../../dart/package_map.dart';
......@@ -394,3 +395,48 @@ abstract class CopyFlutterAotBundle extends Target {
environment.buildDir.childFile('app.so').copySync(outputFile.path);
}
}
/// Lipo CLI tool wrapper shared by iOS and macOS builds.
class Lipo {
/// Static only.
Lipo._();
/// Create a "fat" binary by combining multiple architecture-specific ones.
/// `skipMissingInputs` can be changed to `true` to first check whether
/// the expected input paths exist and ignore the command if they don't.
/// Otherwise, `lipo` would fail if the given paths didn't exist.
static Future<void> create(
Environment environment,
List<DarwinArch> darwinArchs, {
required String relativePath,
required String inputDir,
bool skipMissingInputs = false,
}) async {
final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, relativePath);
environment.fileSystem.directory(resultPath).parent.createSync(recursive: true);
Iterable<String> inputPaths = darwinArchs.map(
(DarwinArch iosArch) => environment.fileSystem.path.join(inputDir, getNameForDarwinArch(iosArch), relativePath)
);
if (skipMissingInputs) {
inputPaths = inputPaths.where(environment.fileSystem.isFileSync);
if (inputPaths.isEmpty) {
return;
}
}
final List<String> lipoArgs = <String>[
'lipo',
...inputPaths,
'-create',
'-output',
resultPath,
];
final ProcessResult result = await environment.processManager.run(lipoArgs);
if (result.exitCode != 0) {
throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}');
}
}
}
......@@ -112,19 +112,24 @@ abstract class AotAssemblyBase extends Target {
if (results.any((int result) => result != 0)) {
throw Exception('AOT snapshotter exited with code ${results.join()}');
}
final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App');
environment.fileSystem.directory(resultPath).parent.createSync(recursive: true);
final ProcessResult result = await environment.processManager.run(<String>[
'lipo',
...darwinArchs.map((DarwinArch iosArch) =>
environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')),
'-create',
'-output',
resultPath,
]);
if (result.exitCode != 0) {
throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}');
}
// Combine the app lib into a fat framework.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework/App',
inputDir: buildOutputPath,
);
// And combine the dSYM for each architecture too, if it was created.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework.dSYM/Contents/Resources/DWARF/App',
inputDir: buildOutputPath,
// Don't fail if the dSYM wasn't created (i.e. during a debug build).
skipMissingInputs: true,
);
}
}
......@@ -489,6 +494,26 @@ abstract class IosAssetBundle extends Target {
.copySync(frameworkBinaryPath);
}
// Copy the dSYM
if (environment.buildDir.childDirectory('App.framework.dSYM').existsSync()) {
final File dsymOutputBinary = environment
.outputDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App');
dsymOutputBinary.parent.createSync(recursive: true);
environment
.buildDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App')
.copySync(dsymOutputBinary.path);
}
// Copy the assets.
final Depfile assetDepfile = await copyAssets(
environment,
......@@ -547,8 +572,25 @@ class DebugIosApplicationBundle extends IosAssetBundle {
];
}
/// IosAssetBundle with debug symbols, used for Profile and Release builds.
abstract class _IosAssetBundleWithDSYM extends IosAssetBundle {
const _IosAssetBundleWithDSYM();
@override
List<Source> get inputs => <Source>[
...super.inputs,
const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
@override
List<Source> get outputs => <Source>[
...super.outputs,
const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
}
/// Build a profile iOS application bundle.
class ProfileIosApplicationBundle extends IosAssetBundle {
class ProfileIosApplicationBundle extends _IosAssetBundleWithDSYM {
const ProfileIosApplicationBundle();
@override
......@@ -561,7 +603,7 @@ class ProfileIosApplicationBundle extends IosAssetBundle {
}
/// Build a release iOS application bundle.
class ReleaseIosApplicationBundle extends IosAssetBundle {
class ReleaseIosApplicationBundle extends _IosAssetBundleWithDSYM {
const ReleaseIosApplicationBundle();
@override
......
......@@ -302,19 +302,23 @@ class CompileMacOSFramework extends Target {
throw Exception('AOT snapshotter exited with code ${results.join()}');
}
final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App');
environment.fileSystem.directory(resultPath).parent.createSync(recursive: true);
final ProcessResult result = await environment.processManager.run(<String>[
'lipo',
...darwinArchs.map((DarwinArch iosArch) =>
environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')),
'-create',
'-output',
resultPath,
]);
if (result.exitCode != 0) {
throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}');
}
// Combine the app lib into a fat framework.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework/App',
inputDir: buildOutputPath,
);
// And combine the dSYM for each architecture too, if it was created.
await Lipo.create(
environment,
darwinArchs,
relativePath: 'App.framework.dSYM/Contents/Resources/DWARF/App',
inputDir: buildOutputPath,
// Don't fail if the dSYM wasn't created (i.e. during a debug build).
skipMissingInputs: true,
);
}
@override
......@@ -332,6 +336,7 @@ class CompileMacOSFramework extends Target {
@override
List<Source> get outputs => const <Source>[
Source.pattern('{BUILD_DIR}/App.framework/App'),
Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
}
......@@ -382,6 +387,26 @@ abstract class MacOSBundleFlutterAssets extends Target {
.childFile('App')
.copySync(outputDirectory.childFile('App').path);
// Copy the dSYM
if (environment.buildDir.childDirectory('App.framework.dSYM').existsSync()) {
final File dsymOutputBinary = environment
.outputDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App');
dsymOutputBinary.parent.createSync(recursive: true);
environment
.buildDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App')
.copySync(dsymOutputBinary.path);
}
// Copy assets into asset directory.
final Directory assetDirectory = outputDirectory
.childDirectory('Resources')
......@@ -530,6 +555,18 @@ class ProfileMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets {
CompileMacOSFramework(),
ProfileUnpackMacOS(),
];
@override
List<Source> get inputs => <Source>[
...super.inputs,
const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
@override
List<Source> get outputs => <Source>[
...super.outputs,
const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
}
......@@ -546,6 +583,18 @@ class ReleaseMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets {
ReleaseUnpackMacOS(),
];
@override
List<Source> get inputs => <Source>[
...super.inputs,
const Source.pattern('{BUILD_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
@override
List<Source> get outputs => <Source>[
...super.outputs,
const Source.pattern('{OUTPUT_DIR}/App.framework.dSYM/Contents/Resources/DWARF/App'),
];
@override
Future<void> build(Environment environment) async {
bool buildSuccess = true;
......
......@@ -68,8 +68,11 @@ class SymbolizeCommand extends FlutterCommand {
if (argResults?.wasParsed('debug-info') != true) {
throwToolExit('"--debug-info" is required to symbolize stack traces.');
}
if (!_fileSystem.isFileSync(stringArgDeprecated('debug-info')!)) {
throwToolExit('${stringArgDeprecated('debug-info')} does not exist.');
final String debugInfoPath = stringArgDeprecated('debug-info')!;
if (debugInfoPath.endsWith('.dSYM')
? !_fileSystem.isDirectorySync(debugInfoPath)
: !_fileSystem.isFileSync(debugInfoPath)) {
throwToolExit('$debugInfoPath does not exist.');
}
if ((argResults?.wasParsed('input') ?? false) && !_fileSystem.isFileSync(stringArgDeprecated('input')!)) {
throwToolExit('${stringArgDeprecated('input')} does not exist.');
......@@ -105,7 +108,25 @@ class SymbolizeCommand extends FlutterCommand {
input = _stdio.stdin;
}
final Uint8List symbols = _fileSystem.file(stringArgDeprecated('debug-info')).readAsBytesSync();
String debugInfoPath = stringArgDeprecated('debug-info')!;
// If it's a dSYM container, expand the path to the actual DWARF.
if (debugInfoPath.endsWith('.dSYM')) {
final Directory debugInfoDir = _fileSystem
.directory(debugInfoPath)
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF');
final List<FileSystemEntity> dwarfFiles = debugInfoDir.listSync().whereType<File>().toList();
if (dwarfFiles.length == 1) {
debugInfoPath = dwarfFiles.first.path;
} else {
throwToolExit('Expected a single DWARF file in a dSYM container.');
}
}
final Uint8List symbols = _fileSystem.file(debugInfoPath).readAsBytesSync();
await _dwarfSymbolizationService.decode(
input: input,
output: output,
......
......@@ -165,16 +165,17 @@ class Xcode {
/// See [XcodeProjectInterpreter.xcrunCommand].
List<String> xcrunCommand() => _xcodeProjectInterpreter.xcrunCommand();
Future<RunResult> cc(List<String> args) {
return _processUtils.run(
<String>[...xcrunCommand(), 'cc', ...args],
throwOnError: true,
);
}
Future<RunResult> cc(List<String> args) => _run('cc', args);
Future<RunResult> clang(List<String> args) => _run('clang', args);
Future<RunResult> dsymutil(List<String> args) => _run('dsymutil', args);
Future<RunResult> strip(List<String> args) => _run('strip', args);
Future<RunResult> clang(List<String> args) {
Future<RunResult> _run(String command, List<String> args) {
return _processUtils.run(
<String>[...xcrunCommand(), 'clang', ...args],
<String>[...xcrunCommand(), command, ...args],
throwOnError: true,
);
}
......
......@@ -69,7 +69,7 @@ void main() {
OutputPreferences: () => OutputPreferences.test(),
});
testUsingContext('symbolize exits when --debug-info file is missing', () async {
testUsingContext('symbolize exits when --debug-info dwarf file is missing', () async {
final SymbolizeCommand command = SymbolizeCommand(
stdio: stdio,
fileSystem: fileSystem,
......@@ -83,6 +83,20 @@ void main() {
OutputPreferences: () => OutputPreferences.test(),
});
testUsingContext('symbolize exits when --debug-info dSYM is missing', () async {
final SymbolizeCommand command = SymbolizeCommand(
stdio: stdio,
fileSystem: fileSystem,
dwarfSymbolizationService: DwarfSymbolizationService.test(),
);
final Future<void> result = createTestCommandRunner(command)
.run(const <String>['symbolize', '--debug-info=app.dSYM']);
expect(result, throwsToolExit(message: 'app.dSYM does not exist.'));
}, overrides: <Type, Generator>{
OutputPreferences: () => OutputPreferences.test(),
});
testUsingContext('symbolize exits when --input file is missing', () async {
final SymbolizeCommand command = SymbolizeCommand(
stdio: stdio,
......
......@@ -210,7 +210,6 @@ void main() {
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=$assembly',
'--strip',
'main.dill',
]),
kWhichSysctlCommand,
......@@ -253,6 +252,21 @@ void main() {
'build/foo/App.framework/App',
'build/foo/snapshot_assembly.o',
]),
const FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
'build/foo/App.framework.dSYM',
'build/foo/App.framework/App',
]),
const FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
'build/foo/App.framework/App',
'-o',
'build/foo/App.framework/App',
]),
]);
final int genSnapshotExitCode = await snapshotter.build(
......@@ -285,7 +299,6 @@ void main() {
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=$assembly',
'--strip',
'--dwarf-stack-traces',
'--save-debugging-info=$debugPath',
'main.dill',
......@@ -312,6 +325,21 @@ void main() {
'arm64',
...kDefaultClang,
]),
const FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
'build/foo/App.framework.dSYM',
'build/foo/App.framework/App',
]),
const FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
'build/foo/App.framework/App',
'-o',
'build/foo/App.framework/App',
]),
]);
final int genSnapshotExitCode = await snapshotter.build(
......@@ -344,7 +372,6 @@ void main() {
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=$assembly',
'--strip',
'--obfuscate',
'main.dill',
]),
......@@ -370,6 +397,21 @@ void main() {
'arm64',
...kDefaultClang,
]),
const FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
'build/foo/App.framework.dSYM',
'build/foo/App.framework/App',
]),
const FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
'build/foo/App.framework/App',
'-o',
'build/foo/App.framework/App',
]),
]);
final int genSnapshotExitCode = await snapshotter.build(
......@@ -400,7 +442,6 @@ void main() {
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=${fileSystem.path.join(outputPath, 'snapshot_assembly.S')}',
'--strip',
'main.dill',
]),
kWhichSysctlCommand,
......@@ -425,6 +466,21 @@ void main() {
'arm64',
...kDefaultClang,
]),
const FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
'build/foo/App.framework.dSYM',
'build/foo/App.framework/App',
]),
const FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
'build/foo/App.framework/App',
'-o',
'build/foo/App.framework/App',
]),
]);
final int genSnapshotExitCode = await snapshotter.build(
......
......@@ -477,7 +477,6 @@ void main() {
'--deterministic',
kAssemblyAot,
'--assembly=$build/arm64/snapshot_assembly.S',
'--strip',
'$build/app.dill',
]),
FakeCommand(command: <String>[
......@@ -520,6 +519,21 @@ void main() {
'$build/arm64/App.framework/App',
'$build/arm64/snapshot_assembly.o',
]),
FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
'$build/arm64/App.framework.dSYM',
'$build/arm64/App.framework/App',
]),
FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
'$build/arm64/App.framework/App',
'-o',
'$build/arm64/App.framework/App',
]),
FakeCommand(command: <String>[
'lipo',
'$build/arm64/App.framework/App',
......@@ -553,7 +567,6 @@ void main() {
'--trace-precompiler-to=code_size_1/trace.arm64.json',
kAssemblyAot,
'--assembly=$build/arm64/snapshot_assembly.S',
'--strip',
'$build/app.dill',
]),
FakeCommand(command: <String>[
......@@ -596,6 +609,21 @@ void main() {
'$build/arm64/App.framework/App',
'$build/arm64/snapshot_assembly.o',
]),
FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
'$build/arm64/App.framework.dSYM',
'$build/arm64/App.framework/App',
]),
FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
'$build/arm64/App.framework/App',
'-o',
'$build/arm64/App.framework/App',
]),
FakeCommand(command: <String>[
'lipo',
'$build/arm64/App.framework/App',
......
......@@ -239,6 +239,14 @@ void main() {
.childFile('App')
.createSync(recursive: true);
// Input dSYM
environment.buildDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App')
.createSync(recursive: true);
final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
final File frameworkDirectoryBinary = frameworkDirectory.childFile('App');
......@@ -257,6 +265,12 @@ void main() {
expect(frameworkDirectoryBinary, exists);
expect(frameworkDirectory.childFile('Info.plist'), exists);
expect(environment.outputDir
.childDirectory('App.framework.dSYM')
.childDirectory('Contents')
.childDirectory('Resources')
.childDirectory('DWARF')
.childFile('App'), exists);
final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
expect(assetDirectory.childFile('kernel_blob.bin'), isNot(exists));
......
......@@ -295,6 +295,28 @@ void main() {
ProcessManager: () => processManager,
});
testUsingContext('release macOS application creates App.framework.dSYM', () async {
fileSystem.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin')
.createSync(recursive: true);
fileSystem.file('bin/cache/artifacts/engine/darwin-x64/isolate_snapshot.bin')
.createSync(recursive: true);
fileSystem.file('${environment.buildDir.path}/App.framework/App')
.createSync(recursive: true);
fileSystem.file('${environment.buildDir.path}/App.framework.dSYM/Contents/Resources/DWARF/App')
.createSync(recursive: true);
await const ReleaseMacOSBundleFlutterAssets()
.build(environment..defines[kBuildMode] = 'release');
expect(fileSystem.file(
'App.framework.dSYM/Contents/Resources/DWARF/App'),
exists,
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
});
testUsingContext('release/profile macOS application updates when App.framework updates', () async {
fileSystem.file('bin/cache/artifacts/engine/darwin-x64/vm_isolate_snapshot.bin')
.createSync(recursive: true);
......@@ -415,13 +437,20 @@ void main() {
environment.defines[kDarwinArchs] = 'arm64 x86_64';
environment.defines[kBuildMode] = 'release';
// Input dSYMs need to exist for `lipo` to combine them
environment.buildDir
.childFile('arm64/App.framework.dSYM/Contents/Resources/DWARF/App')
.createSync(recursive: true);
environment.buildDir
.childFile('x86_64/App.framework.dSYM/Contents/Resources/DWARF/App')
.createSync(recursive: true);
processManager.addCommands(<FakeCommand>[
FakeCommand(command: <String>[
'Artifact.genSnapshot.TargetPlatform.darwin.release_arm64',
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}',
'--strip',
environment.buildDir.childFile('app.dill').path,
]),
FakeCommand(command: <String>[
......@@ -429,7 +458,6 @@ void main() {
'--deterministic',
'--snapshot_kind=app-aot-assembly',
'--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}',
'--strip',
environment.buildDir.childFile('app.dill').path,
]),
FakeCommand(command: <String>[
......@@ -458,6 +486,36 @@ void main() {
'-o', environment.buildDir.childFile('x86_64/App.framework/App').path,
environment.buildDir.childFile('x86_64/snapshot_assembly.o').path,
]),
FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
environment.buildDir.childFile('arm64/App.framework.dSYM').path,
environment.buildDir.childFile('arm64/App.framework/App').path,
]),
FakeCommand(command: <String>[
'xcrun',
'dsymutil',
'-o',
environment.buildDir.childFile('x86_64/App.framework.dSYM').path,
environment.buildDir.childFile('x86_64/App.framework/App').path,
]),
FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
environment.buildDir.childFile('arm64/App.framework/App').path,
'-o',
environment.buildDir.childFile('arm64/App.framework/App').path,
]),
FakeCommand(command: <String>[
'xcrun',
'strip',
'-S',
environment.buildDir.childFile('x86_64/App.framework/App').path,
'-o',
environment.buildDir.childFile('x86_64/App.framework/App').path,
]),
FakeCommand(command: <String>[
'lipo',
environment.buildDir.childFile('arm64/App.framework/App').path,
......@@ -466,6 +524,14 @@ void main() {
'-output',
environment.buildDir.childFile('App.framework/App').path,
]),
FakeCommand(command: <String>[
'lipo',
environment.buildDir.childFile('arm64/App.framework.dSYM/Contents/Resources/DWARF/App').path,
environment.buildDir.childFile('x86_64/App.framework.dSYM/Contents/Resources/DWARF/App').path,
'-create',
'-output',
environment.buildDir.childFile('App.framework.dSYM/Contents/Resources/DWARF/App').path,
]),
]);
await const CompileMacOSFramework().build(environment);
......
......@@ -5,6 +5,7 @@
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/utils.dart';
import 'package:flutter_tools/src/build_info.dart';
import '../integration.shard/test_utils.dart';
......@@ -75,7 +76,7 @@ void main() {
for (final BuildMode buildMode in <BuildMode>[BuildMode.debug, BuildMode.release]) {
group('build in ${buildMode.name} mode', () {
late Directory buildPath;
late Directory outputPath;
late Directory outputApp;
late Directory frameworkDirectory;
late Directory outputFlutterFramework;
......@@ -83,6 +84,9 @@ void main() {
late Directory outputAppFramework;
late File outputAppFrameworkBinary;
late File outputPluginFrameworkBinary;
late Directory buildPath;
late Directory buildAppFrameworkDsym;
late File buildAppFrameworkDsymBinary;
late ProcessResult buildResult;
setUpAll(() {
......@@ -98,14 +102,14 @@ void main() {
'--split-debug-info=foo debug info/',
], workingDirectory: projectRoot);
buildPath = fileSystem.directory(fileSystem.path.join(
outputPath = fileSystem.directory(fileSystem.path.join(
projectRoot,
'build',
'ios',
'iphoneos',
));
outputApp = buildPath.childDirectory('Runner.app');
outputApp = outputPath.childDirectory('Runner.app');
frameworkDirectory = outputApp.childDirectory('Frameworks');
outputFlutterFramework = frameworkDirectory.childDirectory('Flutter.framework');
......@@ -115,6 +119,16 @@ void main() {
outputAppFrameworkBinary = outputAppFramework.childFile('App');
outputPluginFrameworkBinary = frameworkDirectory.childDirectory('hello.framework').childFile('hello');
buildPath = fileSystem.directory(fileSystem.path.join(
projectRoot,
'build',
'ios',
'${sentenceCase(buildMode.name)}-iphoneos',
));
buildAppFrameworkDsym = buildPath.childDirectory('App.framework.dSYM');
buildAppFrameworkDsymBinary = buildAppFrameworkDsym.childFile('Contents/Resources/DWARF/App');
});
testWithoutContext('flutter build ios builds a valid app', () {
......@@ -128,6 +142,8 @@ void main() {
expect(outputAppFrameworkBinary, exists);
expect(outputAppFramework.childFile('Info.plist'), exists);
expect(buildAppFrameworkDsymBinary.existsSync(), buildMode != BuildMode.debug);
final File vmSnapshot = fileSystem.file(fileSystem.path.join(
outputAppFramework.path,
'flutter_assets',
......@@ -190,6 +206,25 @@ void main() {
expect(aotSymbolsFound, buildMode != BuildMode.debug);
});
// dSYM is not created for a debug build so nothing to check.
if (buildMode != BuildMode.debug) {
testWithoutContext('check symbols in dSYM', () {
final ProcessResult nm = processManager.runSync(
<String>[
'nm',
'--debug-syms',
'--defined-only',
'--just-symbol-name',
buildAppFrameworkDsymBinary.path,
'-arch',
'arm64',
],
);
final List<String> symbols = (nm.stdout as String).split('\n');
expect(symbols, contains('_kDartVmSnapshotInstructions'));
});
}
testWithoutContext('xcode_backend embed_and_thin', () {
outputFlutterFramework.deleteSync(recursive: true);
outputAppFramework.deleteSync(recursive: true);
......@@ -219,7 +254,7 @@ void main() {
'ios',
'Release-iphoneos',
),
'TARGET_BUILD_DIR': buildPath.path,
'TARGET_BUILD_DIR': outputPath.path,
'FRAMEWORKS_FOLDER_PATH': 'Runner.app/Frameworks',
'VERBOSE_SCRIPT_LOGGING': '1',
'FLUTTER_BUILD_MODE': 'release',
......
......@@ -67,18 +67,18 @@ void main() {
expect(result.exitCode, 0);
expect(result.stdout, contains('Running pod install'));
expect(podfile.lastModifiedSync().isBefore(podfileLock.lastModifiedSync()), isTrue);
final Directory outputApp = fileSystem.directory(fileSystem.path.join(
final Directory buildPath = fileSystem.directory(fileSystem.path.join(
workingDirectory,
'build',
'macos',
'Build',
'Products',
buildMode,
'flutter_gallery.app',
));
expect(podfile.lastModifiedSync().isBefore(podfileLock.lastModifiedSync()), isTrue);
final Directory outputApp = buildPath.childDirectory('flutter_gallery.app');
final Directory outputAppFramework =
fileSystem.directory(fileSystem.path.join(
outputApp.path,
......@@ -87,19 +87,19 @@ void main() {
'App.framework',
));
final File outputAppFrameworkBinary = outputAppFramework.childFile('App');
final String archs = processManager.runSync(
<String>['file', outputAppFrameworkBinary.path],
).stdout as String;
final bool containsX64 = archs.contains('Mach-O 64-bit dynamically linked shared library x86_64');
final bool containsArm = archs.contains('Mach-O 64-bit dynamically linked shared library arm64');
if (buildModeLower == 'debug') {
// Only build the architecture matching the machine running this test, not both.
expect(containsX64 ^ containsArm, isTrue, reason: 'Unexpected architecture $archs');
} else {
expect(containsX64, isTrue, reason: 'Unexpected architecture $archs');
expect(containsArm, isTrue, reason: 'Unexpected architecture $archs');
_checkFatBinary(
outputAppFramework.childFile('App'),
buildModeLower,
'dynamically linked shared library',
);
// dSYM is not created for a debug build so nothing to check.
if (buildMode != 'Debug') {
_checkFatBinary(
buildPath.childFile('App.framework.dSYM/Contents/Resources/DWARF/App'),
buildModeLower,
'dSYM companion file',
);
}
expect(outputAppFramework.childLink('Resources'), exists);
......@@ -172,3 +172,19 @@ void main() {
}, skip: !platform.isMacOS); // [intended] only makes sense for macos platform.
}
}
void _checkFatBinary(File file, String buildModeLower, String expectedType) {
final String archs = processManager.runSync(
<String>['file', file.path],
).stdout as String;
final bool containsX64 = archs.contains('Mach-O 64-bit $expectedType x86_64');
final bool containsArm = archs.contains('Mach-O 64-bit $expectedType arm64');
if (buildModeLower == 'debug') {
// Only build the architecture matching the machine running this test, not both.
expect(containsX64 ^ containsArm, isTrue, reason: 'Unexpected architecture $archs');
} else {
expect(containsX64, isTrue, reason: 'Unexpected architecture $archs');
expect(containsArm, isTrue, reason: 'Unexpected architecture $archs');
}
}
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