| // 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)), |
| ); |
| } |
| } |
| }; |