blob: 798cf4ac2d25e940c744c1712b714bbe5cca1850 [file] [log] [blame] [edit]
// Copyright (c) 2023, 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.
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:logging/logging.dart';
import 'package:native_test_helpers/native_test_helpers.dart';
import 'package:native_toolchain_c/src/native_toolchain/apple_clang.dart';
import 'package:native_toolchain_c/src/native_toolchain/msvc.dart';
import 'package:native_toolchain_c/src/utils/run_process.dart';
import 'package:test/test.dart';
/// Returns a suffix for a test that is parameterized.
///
/// [tags] represent the current configuration of the test. Each element
/// is converted to a string by calling [Object.toString].
///
/// ## Example
///
/// The instances of the test below will have the following descriptions:
///
/// - `My test (debug)`
/// - `My test (release)`
///
/// ```dart
/// void main() {
/// for (final buildMode in BuildMode.values) {
/// final suffix = testSuffix([buildMode]);
///
/// test('My test$suffix', () {});
/// }
/// }
/// ```
String testSuffix(List<Object> tags) => switch (tags) {
[] => '',
_ => ' (${tags.join(', ')})',
};
const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES';
Future<Uri> tempDirForTest({String? prefix, bool keepTemp = false}) async {
final tempDir = await Directory.systemTemp.createTemp(prefix);
// Deal with Windows temp folder aliases.
final tempUri = Directory(
await tempDir.resolveSymbolicLinks(),
).uri.normalizePath();
if ((!Platform.environment.containsKey(keepTempKey) ||
Platform.environment[keepTempKey]!.isEmpty) &&
!keepTemp) {
addTearDown(() => tempDir.delete(recursive: true));
} else {
addTearDown(() => print('$keepTempKey $tempUri'));
}
return tempUri;
}
/// Logger that outputs the full trace when a test fails.
Logger get logger => _logger ??= () {
// A new logger is lazily created for each test so that the messages
// captured by printOnFailure are scoped to the correct test.
addTearDown(() => _logger = null);
return _createTestLogger();
}();
Logger? _logger;
Logger createCapturingLogger(List<String> capturedMessages) =>
_createTestLogger(capturedMessages: capturedMessages);
Logger _createTestLogger({List<String>? capturedMessages}) =>
Logger.detached('')
..level = Level.ALL
..onRecord.listen((record) {
printOnFailure(
'${record.level.name}: ${record.time}: ${record.message}',
);
capturedMessages?.add(record.message);
});
Uri packageUri = findPackageRoot('native_toolchain_c');
/// Archiver provided by the environment.
///
/// Provided on Dart CI.
final Uri? _ar = Platform.environment['DART_HOOK_TESTING_C_COMPILER__AR']
?.asFileUri();
/// Compiler provided by the environment.
///
/// Provided on Dart CI.
final Uri? _cc = Platform.environment['DART_HOOK_TESTING_C_COMPILER__CC']
?.asFileUri();
/// Linker provided by the environment.
///
/// Provided on Dart CI.
final Uri? _ld = Platform.environment['DART_HOOK_TESTING_C_COMPILER__LD']
?.asFileUri();
/// Path to script that sets environment variables for [_cc], [_ld], and [_ar].
///
/// Provided on Dart CI.
final Uri? _envScript = Platform
.environment['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT']
?.asFileUri();
/// Arguments for [_envScript] provided by environment.
///
/// Provided on Dart CI.
final List<String>? _envScriptArgs = Platform
.environment['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT_ARGUMENTS']
?.split(' ');
/// Configuration for the native toolchain.
///
/// Provided on Dart CI.
final cCompiler = (_cc == null || _ar == null || _ld == null)
? null
: CCompilerConfig(
compiler: _cc!,
archiver: _ar!,
linker: _ld!,
windows: WindowsCCompilerConfig(
developerCommandPrompt: _envScript == null
? null
: DeveloperCommandPrompt(
script: _envScript!,
arguments: _envScriptArgs ?? [],
),
),
);
extension on String {
Uri asFileUri() => Uri.file(this);
}
/// Looks up the install name of a dynamic library at [libraryUri].
///
/// Because `otool` output multiple names, [libraryName] as search parameter.
Future<String> runOtoolInstallName(Uri libraryUri, String libraryName) async {
final otoolUri = (await otool.defaultResolver!.resolve(
logger: logger,
)).first.uri;
final otoolResult = await runProcess(
executable: otoolUri,
arguments: ['-l', libraryUri.path],
logger: logger,
);
expect(otoolResult.exitCode, 0);
// Leading space on purpose to differentiate from other types of names.
const installNameName = ' name ';
final installName = otoolResult.stdout
.split('\n')
.firstWhere((e) => e.contains(installNameName) && e.contains(libraryName))
.trim()
.split(' ')[1];
return installName;
}
/// Opens the [DynamicLibrary] at [path] and register a tear down hook to close
/// it when the current test is done.
DynamicLibrary openDynamicLibraryForTest(String path) {
final library = DynamicLibrary.open(path);
addTearDown(library.close);
return library;
}
extension UnescapePath on String {
String unescape() => replaceAll('\\', '/');
}
Future<String> readelfMachine(String path) async {
final result = await readelf(path, 'h');
return result.split('\n').firstWhere((e) => e.contains('Machine:'));
}
const readElfMachine = {
Architecture.arm: 'ARM',
Architecture.arm64: 'AArch64',
Architecture.ia32: 'Intel 80386',
Architecture.x64: 'Advanced Micro Devices X86-64',
Architecture.riscv64: 'RISC-V',
};
Future<String> readelf(String filePath, String flags) async {
final result = await runProcess(
executable: Uri.file('readelf'),
arguments: ['-$flags', filePath],
logger: logger,
);
expect(result.exitCode, 0);
return result.stdout;
}
List<String> nmParameterFor(OS targetOS) => switch (targetOS) {
OS.macOS || OS.iOS => const [],
OS() => ['-D'],
};
/// Returns null if the tool to extract the symbols is not available.
Future<String?> readSymbols(CodeAsset asset, OS targetOS) async {
final assetUri = asset.file!;
switch (targetOS) {
case OS.windows:
final result = await _runDumpbin(['/EXPORTS'], asset.file!);
if (result == null) {
return null;
}
expect(result.exitCode, 0);
return result.stdout;
case OS():
final result = await runProcess(
executable: Uri(path: 'nm'),
arguments: [...nmParameterFor(targetOS), assetUri.toFilePath()],
logger: logger,
);
expect(result.exitCode, 0);
return result.stdout;
}
}
/// Returns null if the dumpbin tool is not available.
Future<RunProcessResult?> _runDumpbin(
List<String> arguments,
Uri libUri,
) async {
final dumpbinTools = await dumpbin.defaultResolver!.resolve(logger: logger);
if (dumpbinTools.isEmpty) {
logger.info('Unable to locate dumpbin tool. Some expects may be skipped.');
return null;
}
return await runProcess(
executable: dumpbinTools.first.uri,
arguments: [...arguments, libUri.toFilePath()],
logger: logger,
);
}
Future<int> textSectionAddress(Uri dylib) async {
if (Platform.isMacOS) {
// Find the line in the objdump output that looks like:
// 11 .text 00000046 00000000000045a0 TEXT
final result = await runProcess(
executable: Uri.file('objdump'),
arguments: ['--headers', dylib.toFilePath()],
logger: logger,
);
expect(result.exitCode, 0);
final textSection = result.stdout
.split('\n')
.firstWhere((e) => e.contains('.text'));
final parsed = textSection.split(' ').where((e) => e.isNotEmpty).toList();
expect(parsed[1], '.text');
expect(parsed[4], 'TEXT');
final vma = int.parse(parsed[3], radix: 16);
return vma;
}
if (Platform.isLinux) {
// Find the line in the readelf output that looks like:
// [11] .text PROGBITS 00004328 000328 000064 00 AX 0 0 4
final result = await readelf(dylib.toFilePath(), 'S');
final textSection = result
.split('\n')
.firstWhere((e) => e.contains('.text'));
final parsed = textSection.split(' ').where((e) => e.isNotEmpty).toList();
expect(parsed[1], '.text');
expect(parsed[2], 'PROGBITS');
final addr = int.parse(parsed[3], radix: 16);
return addr;
}
throw UnimplementedError();
}
Future<void> expectPageSize(Uri dylib, int pageSize) async {
if (Platform.isMacOS || Platform.isLinux) {
// If page size is 16kb, the `.text` section address should be
// above 0x4000. With smaller page sizes it's above 0x1000.
final vma = await textSectionAddress(dylib);
expect(vma, greaterThanOrEqualTo(pageSize));
}
}
int defaultMacOSVersion = 13;
/// From https://docs.flutter.dev/reference/supported-platforms.
const flutterAndroidNdkVersionLowestSupported = 21;
/// From https://docs.flutter.dev/reference/supported-platforms.
const flutterAndroidNdkVersionHighestSupported = 34;
/// File-format strings used by the `objdump` tool for Android binaries that
/// run on a given architecture.
const objdumpFileFormatAndroid = {
Architecture.arm: 'elf32-littlearm',
Architecture.arm64: 'elf64-littleaarch64',
Architecture.ia32: 'elf32-i386',
Architecture.x64: 'elf64-x86-64',
Architecture.riscv64: 'elf64-littleriscv',
};
const objdumpFileFormatMacOS = {
Architecture.arm64: 'mach-o arm64',
Architecture.x64: 'mach-o 64-bit x86-64',
};
// Don't include 'mach-o' or 'Mach-O', different spelling is used.
const objdumpFileFormatIOS = {
Architecture.arm64: 'arm64',
Architecture.x64: '64-bit x86-64',
};
const targetOSToObjdumpFileFormat = {
OS.android: objdumpFileFormatAndroid,
OS.macOS: objdumpFileFormatMacOS,
OS.iOS: objdumpFileFormatMacOS,
};
const dumpbinFileFormat = {
Architecture.arm64: 'ARM64',
Architecture.ia32: 'x86',
Architecture.x64: 'x64',
};
/// Checks that the provided [libUri] binary has the correct format to be
/// executed on the provided [targetArch] architecture.
///
/// On Linux, the format of the binary is determined by `readelf`. On MacOS,
/// the `objdump` tool is used. On Windows, `dumpbin` is used.
Future<void> expectMachineArchitecture(
Uri libUri,
Architecture targetArch,
OS targetOS,
) async {
if (Platform.isLinux) {
final machine = await readelfMachine(libUri.path);
expect(machine, contains(readElfMachine[targetArch]));
} else if (Platform.isMacOS) {
final result = await runProcess(
executable: Uri.file('objdump'),
arguments: ['-T', libUri.path],
logger: logger,
);
expect(result.exitCode, 0);
final machine = result.stdout
.split('\n')
.firstWhere((e) => e.contains('file format'));
expect(
machine,
contains(targetOSToObjdumpFileFormat[targetOS]![targetArch]),
);
} else if (Platform.isWindows && targetOS == OS.windows) {
final result = await _runDumpbin(['/HEADERS'], libUri);
final skipReason = result == null
? 'tool to determine binary architecture unavailable'
: false;
expect(result?.exitCode, 0, skip: skipReason);
final machine = result?.stdout
.split('\n')
.firstWhere((e) => e.contains('machine'));
expect(machine, contains(dumpbinFileFormat[targetArch]), skip: skipReason);
}
}
List<Architecture> supportedArchitecturesFor(OS targetOS) => switch (targetOS) {
OS.macOS || OS.iOS => [Architecture.arm64, Architecture.x64],
OS.windows => [
// TODO(https://github.com/dart-lang/native/issues/170): Support arm64.
// Architecture.arm64,
Architecture.ia32,
Architecture.x64,
],
OS() => [
Architecture.arm,
Architecture.arm64,
Architecture.ia32,
Architecture.x64,
Architecture.riscv64,
],
};
List<Architecture> iOSSupportedArchitecturesFor(IOSSdk iosSdk) =>
switch (iosSdk) {
IOSSdk.iPhoneOS => supportedArchitecturesFor(
OS.iOS,
)..remove(Architecture.x64),
IOSSdk.iPhoneSimulator => supportedArchitecturesFor(OS.iOS),
IOSSdk() => throw UnimplementedError(),
};
const flutteriOSHighestBestEffort = 16;
const flutteriOSHighestSupported = 17;