// 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 'dart:math' as math; 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/tool/tool_resolver.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 tags) => switch (tags) { [] => '', _ => ' (${tags.join(', ')})', }; const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; Future 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(); }(); ToolResolvingContext get systemContext => ToolResolvingContext(logger: logger); Logger? _logger; Logger createCapturingLogger(List capturedMessages) => _createTestLogger(capturedMessages: capturedMessages); Logger createCapturingRecordLogger(List capturedLogs) => _createTestLogger(capturedLogs: capturedLogs); Logger _createTestLogger({ List? capturedMessages, List? capturedLogs, }) => Logger.detached('') ..level = Level.ALL ..onRecord.listen((record) { printOnFailure('${record.level.name}: ${record.time}: ${record.message}'); capturedMessages?.add(record.message); capturedLogs?.add(record); }); 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? _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 runOtoolInstallName(Uri libraryUri, String libraryName) async { final otoolUri = (await otool.defaultResolver!.resolve( systemContext, )).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 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 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 nmParameterFor(OS targetOS) => switch (targetOS) { .macOS || .iOS => const [], OS() => ['-D'], }; /// Returns null if the tool to extract the symbols is not available. Future readSymbols(CodeAsset asset, OS targetOS) async { final assetUri = asset.file!; switch (targetOS) { case .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; } } /// Asserts that [symbol] is not an undefined dynamic symbol in the /// library described by [asset]. /// /// Uses [readSymbols] to inspect the symbol table and checks that /// the symbol does not appear with the `U` (undefined) binding type. Future expectSymbolNotUndefined( CodeAsset asset, OS targetOS, String symbol, ) async { final symbols = await readSymbols(asset, targetOS); if (symbols == null) { // Skip if the tool to extract symbols is not available. return; } final undefinedMatches = symbols .split('\n') .where((line) => line.contains(' U ') && line.contains(symbol)) .toList(); expect( undefinedMatches, isEmpty, reason: '$symbol should not be an undefined symbol', ); } /// Returns null if the dumpbin tool is not available. Future _runDumpbin( List arguments, Uri libUri, ) async { final dumpbinTools = await dumpbin.defaultResolver!.resolve(systemContext); 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 expectPageSize(Uri dylib, int pageSize) async { if (Platform.isMacOS) { // Find the line in the objdump output that looks like: // LOAD off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**14 final result = await runProcess( executable: Uri.file('objdump'), arguments: ['-p', dylib.toFilePath()], logger: logger, ); expect(result.exitCode, 0); final loadHeader = result.stdout .split('\n') .firstWhere((e) => e.contains('LOAD')); final parsed = loadHeader.split(' ').where((e) => e.isNotEmpty).toList(); expect(parsed[7], 'align'); expect(parsed[8], startsWith('2**')); final alignment = math.pow(2, int.parse(parsed[8].substring('2**'.length))); expect(alignment, pageSize); } if (Platform.isLinux) { // The readelf output has the following structure: // Type Offset VirtAddr PhysAddr // FileSiz MemSiz Flags Align // Find the line in the readelf output that looks like: // LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 // 0x0000000000000528 0x0000000000000528 R 0x1000 final result = await readelf(dylib.toFilePath(), 'l'); // Capture the line after the line that contains "LOAD". final regExp = RegExp('LOAD.*\n(.*)'); final loadSegment = regExp.firstMatch(result)!.group(1)!; final alignment = int.parse( loadSegment .split(' ') .where((e) => e.isNotEmpty) .last .substring('0x'.length), radix: 16, ); expect(alignment, 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 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 supportedArchitecturesFor(OS targetOS) => switch (targetOS) { .macOS || .iOS => [.arm64, .x64], .windows => [ // TODO(https://github.com/dart-lang/native/issues/170): Support arm64. // Architecture.arm64, .ia32, .x64, ], OS() => [.arm, .arm64, .ia32, .x64, .riscv64], }; List iOSSupportedArchitecturesFor(IOSSdk iosSdk) => switch (iosSdk) { .iPhoneOS => supportedArchitecturesFor(OS.iOS)..remove(Architecture.x64), .iPhoneSimulator => supportedArchitecturesFor(OS.iOS), IOSSdk() => throw UnimplementedError(), }; const flutteriOSHighestBestEffort = 16; const flutteriOSHighestSupported = 17;