blob: 0dde1fce51b39841ed60177bdc5cc3107c9b67b6 [file]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// This test ensures that using the --macho-reduce-padding flag actually
// reduces the padding used for segments and text/const sections in Mach-O
// outputs.
import "dart:async";
import "dart:io";
import 'package:native_stack_traces/src/macho.dart' show MachO, SegmentCommand;
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'use_flag_test_helper.dart';
Future<void> main() async {
if (!isAOTRuntime) {
return; // Running in JIT: AOT binaries not available.
}
if (Platform.isAndroid) {
return; // SDK tree and dart_bootstrap not available on the test device.
}
if (isSimulator) {
// Output of Mach-O relocatable objects isn't supported for most
// simulated architectures, so don't run the test.
return;
}
if (Platform.isWindows) {
// Currently dsymutil isn't available in this case (but if it is
// in the future, go ahead and run them).
if (llvmTool('dsymutil') == null) return;
}
// These are the tools we need to be available to run on a given platform:
if (!await testExecutable(genSnapshot)) {
throw "Cannot run test as $genSnapshot not available";
}
if (!await testExecutable(dartPrecompiledRuntime)) {
throw "Cannot run test as $dartPrecompiledRuntime not available";
}
if (!File(platformDill).existsSync()) {
throw "Cannot run test as $platformDill does not exist";
}
await withTempDir('macho-reduce-padding', (String tempDir) async {
// We have to use the program in its original location so it can use
// the dart:_internal library (as opposed to adding it as an OtherResources
// option to the test).
final scriptPath = path.join(
sdkDir,
'runtime',
'tests',
'vm',
'dart',
'use_dwarf_stack_traces_flag_program.dart',
);
final scriptDill = path.join(tempDir, 'flag_program.dill');
// Compile script to Kernel IR.
await run(genKernel, <String>[
'--aot',
'--platform=$platformDill',
'-o',
scriptDill,
scriptPath,
]);
final defaultPaddingTestCase = await createTestCase(
tempDir,
scriptDill,
'default_padding',
14,
const [],
);
final reducedPaddingTestCase = await createTestCase(
tempDir,
scriptDill,
'reduced_padding',
6,
const ['--macho-reduce-padding'],
);
test(
"Testing default MachO padding",
checkTestCase(defaultPaddingTestCase),
);
test(
"Testing reduced MachO padding",
checkTestCase(reducedPaddingTestCase),
);
});
}
class TestCase {
int segmentAlignment;
MachO snapshot;
MachO debugInfo;
MachO relocatableObject;
TestCase(
this.segmentAlignment,
this.snapshot,
this.debugInfo,
this.relocatableObject,
);
}
Future<TestCase> createTestCase(
String tempDir,
String scriptDill,
String prefix,
int alignment,
List<String> extraArgs,
) async {
final dsymutil = llvmTool('dsymutil', verbose: true)!;
print("Generating Mach-O snapshots with segment alignment ${1 << alignment}");
final snapshotPath = path.join(tempDir, '$prefix.dylib');
final objectPath = path.join(tempDir, '$prefix.o');
final debugInfoPath = path.join(tempDir, 'debug_info_$prefix.so');
await run(genSnapshot, <String>[
'--dwarf-stack-traces-mode',
'--save-debugging-info=$debugInfoPath',
'--snapshot-kind=app-aot-macho-dylib',
'--macho=$snapshotPath',
'--macho-object=$objectPath',
...extraArgs,
scriptDill,
]);
// Make sure dsymutil doesn't have any issue with the snapshot or
// relocatable object.
print("Generating dSYM for segment alignment ${1 << alignment}");
final dsymPath = path.join(tempDir, '$prefix.dSYM');
await run(dsymutil, ['-o', dsymPath, snapshotPath]);
return TestCase(
alignment,
MachO.fromFile(snapshotPath)!,
MachO.fromFile(debugInfoPath)!,
MachO.fromFile(objectPath)!,
);
}
int align(int n, int alignLog2) {
final alignment = 1 << alignLog2;
final extra = n % alignment;
final padding = (extra == 0) ? 0 : (alignment - extra);
return n + padding;
}
void Function() checkTestCase(TestCase testCase) => () {
for (final macho in [
testCase.snapshot,
testCase.debugInfo,
testCase.relocatableObject,
]) {
for (final segment in macho.commandsWhereType<SegmentCommand>()) {
print(segment);
// Skip the linkedit segment, which isn't padded to a specific alignment.
if (segment.segname == '__LINKEDIT') continue;
int contentsSize = 0;
if (segment.segname == '__TEXT') {
// The header and load commands are contained within the initial (text)
// segment for non-relocatable objects.
contentsSize += macho.header.size + macho.header.sizeofcmds;
}
for (final section in segment.sectionsInOrder) {
expect(
section.align,
lessThanOrEqualTo(testCase.segmentAlignment),
reason:
'Section "${section.segname}", "${section.sectname}" has '
'an alignment of ${section.align} > ${testCase.segmentAlignment}',
);
contentsSize = align(contentsSize, section.align);
contentsSize += section.size;
}
expect(
segment.filesize,
lessThanOrEqualTo(align(contentsSize, testCase.segmentAlignment)),
);
}
}
};