diff --git a/pkgs/native_toolchain_c/CHANGELOG.md b/pkgs/native_toolchain_c/CHANGELOG.md index e85876443..83e4eacb9 100644 --- a/pkgs/native_toolchain_c/CHANGELOG.md +++ b/pkgs/native_toolchain_c/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.16.3 + +* Support linking for Android. + ## 0.16.2 * Bump the SDK constraint to at least the one from `package:hooks` to fix diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart index 8c304fc6f..32ff08df6 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart @@ -53,10 +53,12 @@ class CLinker extends CTool implements Linker { required LinkOutputBuilder output, required Logger? logger, }) async { - if (OS.current != OS.linux || input.config.code.targetOS != OS.linux) { + const supportedTargetOSs = [OS.linux, OS.android]; + if (!supportedTargetOSs.contains(input.config.code.targetOS)) { throw UnsupportedError( - 'Currently, only linux is supported for this ' - 'feature. See also https://github.com/dart-lang/native/issues/1376', + 'This feature is only supported when targeting ' + '${supportedTargetOSs.join(', ')}. ' + 'See also https://github.com/dart-lang/native/issues/1376', ); } final outDir = input.outputDirectory; diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart index 8ef582a8e..2dba6d0f8 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart @@ -9,7 +9,6 @@ import 'package:logging/logging.dart'; import '../native_toolchain/android_ndk.dart'; import '../native_toolchain/apple_clang.dart'; -import '../native_toolchain/clang.dart'; import '../native_toolchain/gcc.dart'; import '../native_toolchain/msvc.dart'; import '../native_toolchain/recognizer.dart'; @@ -63,11 +62,6 @@ class CompilerResolver { final targetArch = codeConfig.targetArchitecture; // TODO(dacoharkes): Support falling back on other tools. - if (targetArch == hostArchitecture && - targetOS == hostOS && - hostOS == OS.linux) { - return clang; - } if (targetOS == OS.macOS || targetOS == OS.iOS) return appleClang; if (targetOS == OS.android) return androidNdkClang; if (hostOS == OS.linux) { @@ -151,11 +145,6 @@ class CompilerResolver { final targetArchitecture = codeConfig.targetArchitecture; // TODO(dacoharkes): Support falling back on other tools. - if (targetArchitecture == hostArchitecture && - targetOS == hostOS && - hostOS == OS.linux) { - return llvmAr; - } if (targetOS == OS.macOS || targetOS == OS.iOS) return appleAr; if (targetOS == OS.android) return androidNdkLlvmAr; if (hostOS == OS.linux) { @@ -240,81 +229,4 @@ class CompilerResolver { ], ); } - - Future resolveLinker() async { - final targetOS = codeConfig.targetOS; - final targetArchitecture = codeConfig.targetArchitecture; - // First, check if the launcher provided a direct path to the compiler. - var result = await _tryLoadLinkerFromInput(); - - // Then, try to detect on the host machine. - final tool = _selectLinker(); - if (tool != null) { - result ??= await _tryLoadToolFromNativeToolchain(tool); - } - - if (result != null) { - return result; - } - - final errorMessage = - "No linker configured on host '${hostOS}_$hostArchitecture' with " - "target '${targetOS}_$targetArchitecture'."; - logger?.severe(errorMessage); - throw ToolError(errorMessage); - } - - Future _tryLoadLinkerFromInput() async { - final inputLdUri = codeConfig.cCompiler?.linker; - if (inputLdUri != null) { - assert(await File.fromUri(inputLdUri).exists()); - logger?.finer( - 'Using linker ${inputLdUri.toFilePath()} ' - 'from cCompiler.ld.', - ); - final tools = await LinkerRecognizer(inputLdUri).resolve(logger: logger); - return tools.first; - } - logger?.finer('No linker set in cCompiler.ld.'); - return null; - } - - /// Select the right compiler for cross compiling to the specified target. - Tool? _selectLinker() { - final targetOS = codeConfig.targetOS; - final targetArchitecture = codeConfig.targetArchitecture; - - if (targetOS == OS.macOS || targetOS == OS.iOS) return appleLd; - if (targetOS == OS.android) return androidNdkLld; - if (hostOS == OS.linux) { - if (Architecture.current == targetArchitecture) { - return lld; - } - switch (targetArchitecture) { - case Architecture.arm: - return armLinuxGnueabihfLd; - case Architecture.arm64: - return aarch64LinuxGnuLd; - case Architecture.ia32: - return i686LinuxGnuLd; - case Architecture.x64: - return x86_64LinuxGnuLd; - case Architecture.riscv64: - return riscv64LinuxGnuLd; - } - } - - if (hostOS == OS.windows) { - switch (targetArchitecture) { - case Architecture.ia32: - return linkIA32; - case Architecture.arm64: - return linkArm64; - case Architecture.x64: - return msvcLink; - } - } - - return null; - } } diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart index eb39c7b7e..0f692f3ef 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart @@ -51,7 +51,7 @@ class LinkerOptions { }) : _linkerFlags = [ ...flags ?? [], '--strip-debug', - if (symbols != null) ...symbols.expand((e) => ['-u', e]), + if (symbols != null) ...symbols.map((e) => '-u,$e'), ].toList(), gcSections = true, _wholeArchiveSandwich = symbols == null, diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart index 0f7dc48cc..fa29d211e 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart @@ -96,8 +96,6 @@ class RunCBuilder { Future archiver() async => (await _resolver.resolveArchiver()).uri; - Future linker() async => await _resolver.resolveLinker(); - Future iosSdk(IOSSdk iosSdk, {required Logger? logger}) async { if (iosSdk == IOSSdk.iPhoneOS) { return (await iPhoneOSSdk.defaultResolver!.resolve( @@ -119,9 +117,7 @@ class RunCBuilder { compiler.uri.resolve('../sysroot/'); Future run() async { - final toolInstance_ = linkerOptions != null - ? await linker() - : await compiler(); + final toolInstance_ = await compiler(); final tool = toolInstance_.tool; if (tool.isClangLike || tool.isLdLike) { await runClangLike(tool: toolInstance_); @@ -333,10 +329,7 @@ class RunCBuilder { // During bundling code assets are all placed in the same directory. // Setting this rpath allows the binary to find other code assets // it is linked against. - if (linkerOptions != null) - '-rpath=\$ORIGIN' - else - '-Wl,-rpath=\$ORIGIN', + '-Wl,-rpath=\$ORIGIN', for (final directory in libraryDirectories) '-L${directory.toFilePath()}', for (final library in libraries) '-l$library', diff --git a/pkgs/native_toolchain_c/pubspec.yaml b/pkgs/native_toolchain_c/pubspec.yaml index fee410af4..090b94daa 100644 --- a/pkgs/native_toolchain_c/pubspec.yaml +++ b/pkgs/native_toolchain_c/pubspec.yaml @@ -1,7 +1,7 @@ name: native_toolchain_c description: >- A library to invoke the native C compiler installed on the host machine. -version: 0.16.2 +version: 0.16.3 repository: https://github.com/dart-lang/native/tree/main/pkgs/native_toolchain_c topics: diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart index 6784bb4ce..8dbf67608 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart @@ -7,7 +7,6 @@ import 'dart:io'; import 'package:code_assets/code_assets.dart'; import 'package:hooks/hooks.dart'; import 'package:native_toolchain_c/native_toolchain_c.dart'; -import 'package:native_toolchain_c/src/utils/run_process.dart'; import 'package:test/test.dart'; import '../helpers.dart'; @@ -21,20 +20,6 @@ void main() { Architecture.riscv64, ]; - const objdumpFileFormat = { - Architecture.arm: 'elf32-littlearm', - Architecture.arm64: 'elf64-littleaarch64', - Architecture.ia32: 'elf32-i386', - Architecture.x64: 'elf64-x86-64', - Architecture.riscv64: 'elf64-littleriscv', - }; - - /// From https://docs.flutter.dev/reference/supported-platforms. - const flutterAndroidNdkVersionLowestSupported = 21; - - /// From https://docs.flutter.dev/reference/supported-platforms. - const flutterAndroidNdkVersionHighestSupported = 34; - const optimizationLevels = OptimizationLevel.values; var selectOptimizationLevel = 0; @@ -58,21 +43,7 @@ void main() { linkMode, optimizationLevel: optimizationLevel, ); - if (Platform.isLinux) { - final machine = await readelfMachine(libUri.path); - expect(machine, contains(readElfMachine[target])); - } 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(objdumpFileFormat[target])); - } + await expectMachineArchitecture(libUri, target); if (linkMode == DynamicLoadingBundled()) { await expectPageSize(libUri, 16 * 1024); } diff --git a/pkgs/native_toolchain_c/test/clinker/build_testfiles.dart b/pkgs/native_toolchain_c/test/clinker/build_testfiles.dart index bc7652612..75d8c4926 100644 --- a/pkgs/native_toolchain_c/test/clinker/build_testfiles.dart +++ b/pkgs/native_toolchain_c/test/clinker/build_testfiles.dart @@ -13,9 +13,11 @@ import '../helpers.dart'; Future buildTestArchive( Uri tempUri, Uri tempUri2, - OS os, - Architecture architecture, -) async { + OS targetOS, + Architecture architecture, { + int? androidTargetNdkApi, // Must be specified iff targetOS is OS.android. +}) async { + assert((targetOS != OS.android) == (androidTargetNdkApi == null)); final test1Uri = packageUri.resolve('test/clinker/testfiles/linker/test1.c'); final test2Uri = packageUri.resolve('test/clinker/testfiles/linker/test2.c'); if (!await File.fromUri(test1Uri).exists() || @@ -27,7 +29,6 @@ Future buildTestArchive( final logMessages = []; final logger = createCapturingLogger(logMessages); - assert(os == OS.linux); // Setup code input for other OSes. final buildInputBuilder = BuildInputBuilder() ..setupShared( packageName: name, @@ -38,10 +39,13 @@ Future buildTestArchive( ..config.setupBuild(linkingEnabled: false) ..addExtension( CodeAssetExtension( - targetOS: os, + targetOS: targetOS, targetArchitecture: architecture, linkModePreference: LinkModePreference.dynamic, cCompiler: cCompiler, + android: androidTargetNdkApi != null + ? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi) + : null, ), ); diff --git a/pkgs/native_toolchain_c/test/clinker/objects_cross_android_test.dart b/pkgs/native_toolchain_c/test/clinker/objects_cross_android_test.dart new file mode 100644 index 000000000..114cd31f6 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/objects_cross_android_test.dart @@ -0,0 +1,30 @@ +// 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. + +import 'package:code_assets/code_assets.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'objects_helper.dart'; + +void main() { + final architectures = [ + Architecture.arm, + Architecture.arm64, + Architecture.ia32, + Architecture.x64, + Architecture.riscv64, + ]; + + const targetOS = OS.android; + + for (final apiLevel in [ + flutterAndroidNdkVersionLowestSupported, + flutterAndroidNdkVersionHighestSupported, + ]) { + group('Android API$apiLevel', () { + runObjectsTests(targetOS, architectures, androidTargetNdkApi: apiLevel); + }); + } +} diff --git a/pkgs/native_toolchain_c/test/clinker/objects_cross_test.dart b/pkgs/native_toolchain_c/test/clinker/objects_cross_test.dart new file mode 100644 index 000000000..f7d44454e --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/objects_cross_test.dart @@ -0,0 +1,32 @@ +// 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. + +//TODO(mosuem): Enable for windows and mac. +// See https://github.com/dart-lang/native/issues/1376. +@TestOn('linux') +library; + +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:test/test.dart'; + +import 'objects_helper.dart'; + +void main() { + if (!Platform.isLinux) { + // Avoid needing status files on Dart SDK CI. + return; + } + + final architectures = [ + Architecture.arm, + Architecture.arm64, + Architecture.ia32, + Architecture.x64, + Architecture.riscv64, + ]..remove(Architecture.current); + + runObjectsTests(OS.current, architectures); +} diff --git a/pkgs/native_toolchain_c/test/clinker/objects_helper.dart b/pkgs/native_toolchain_c/test/clinker/objects_helper.dart new file mode 100644 index 000000000..17036f5d1 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/objects_helper.dart @@ -0,0 +1,78 @@ +// 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. + +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:hooks/hooks.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'build_testfiles.dart'; + +void runObjectsTests( + OS targetOS, + List architectures, { + int? androidTargetNdkApi, // Must be specified iff targetOS is OS.android. +}) { + assert((targetOS != OS.android) == (androidTargetNdkApi == null)); + const name = 'mylibname'; + + for (final architecture in architectures) { + test('link two objects for $architecture', () async { + final tempUri = await tempDirForTest(); + final tempUri2 = await tempDirForTest(); + + final uri = await buildTestArchive( + tempUri, + tempUri2, + targetOS, + architecture, + androidTargetNdkApi: androidTargetNdkApi, + ); + + final linkInputBuilder = LinkInputBuilder() + ..setupShared( + packageName: 'testpackage', + packageRoot: tempUri, + outputFile: tempUri.resolve('output.json'), + outputDirectoryShared: tempUri2, + ) + ..setupLink(assets: [], recordedUsesFile: null) + ..addExtension( + CodeAssetExtension( + targetOS: targetOS, + targetArchitecture: architecture, + linkModePreference: LinkModePreference.dynamic, + cCompiler: cCompiler, + android: androidTargetNdkApi != null + ? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi) + : null, + ), + ); + + final linkInput = linkInputBuilder.build(); + final linkOutput = LinkOutputBuilder(); + + printOnFailure(linkInput.config.code.cCompiler.toString()); + printOnFailure(Platform.environment.keys.toList().toString()); + await CLinker.library( + name: name, + assetName: '', + linkerOptions: LinkerOptions.manual(gcSections: false), + sources: [uri.toFilePath()], + ).run(input: linkInput, output: linkOutput, logger: logger); + + final codeAssets = LinkOutput(linkOutput.json).assets.code; + expect(codeAssets, hasLength(1)); + final asset = codeAssets.first; + expect(asset, isA()); + expect( + await nmReadSymbols(asset), + stringContainsInOrder(['my_func', 'my_other_func']), + ); + }); + } +} diff --git a/pkgs/native_toolchain_c/test/clinker/objects_test.dart b/pkgs/native_toolchain_c/test/clinker/objects_test.dart index 091fae0ec..7c4c318bf 100644 --- a/pkgs/native_toolchain_c/test/clinker/objects_test.dart +++ b/pkgs/native_toolchain_c/test/clinker/objects_test.dart @@ -10,62 +10,15 @@ library; import 'dart:io'; import 'package:code_assets/code_assets.dart'; -import 'package:hooks/hooks.dart'; -import 'package:native_toolchain_c/native_toolchain_c.dart'; import 'package:test/test.dart'; -import '../helpers.dart'; -import 'build_testfiles.dart'; +import 'objects_helper.dart'; -Future main() async { +void main() { if (!Platform.isLinux) { // Avoid needing status files on Dart SDK CI. return; } - final architecture = Architecture.current; - const os = OS.linux; - const name = 'mylibname'; - - test('link two objects', () async { - final tempUri = await tempDirForTest(); - final tempUri2 = await tempDirForTest(); - - final uri = await buildTestArchive(tempUri, tempUri2, os, architecture); - - final linkInputBuilder = LinkInputBuilder() - ..setupShared( - packageName: 'testpackage', - packageRoot: tempUri, - outputFile: tempUri.resolve('output.json'), - outputDirectoryShared: tempUri2, - ) - ..setupLink(assets: [], recordedUsesFile: null) - ..addExtension( - CodeAssetExtension( - targetOS: os, - targetArchitecture: architecture, - linkModePreference: LinkModePreference.dynamic, - cCompiler: cCompiler, - ), - ); - - final linkInput = linkInputBuilder.build(); - final linkOutput = LinkOutputBuilder(); - - printOnFailure(linkInput.config.code.cCompiler.toString()); - printOnFailure(Platform.environment.keys.toList().toString()); - await CLinker.library( - name: name, - assetName: '', - linkerOptions: LinkerOptions.manual(gcSections: false), - sources: [uri.toFilePath()], - ).run(input: linkInput, output: linkOutput, logger: logger); - - final codeAssets = LinkOutput(linkOutput.json).assets.code; - expect(codeAssets, hasLength(1)); - final asset = codeAssets.first; - expect(asset, isA()); - await expectSymbols(asset: asset, symbols: ['my_func', 'my_other_func']); - }); + runObjectsTests(OS.current, [Architecture.current]); } diff --git a/pkgs/native_toolchain_c/test/clinker/throws_test.dart b/pkgs/native_toolchain_c/test/clinker/throws_test.dart index 1a513fd1a..0b2e15a5c 100644 --- a/pkgs/native_toolchain_c/test/clinker/throws_test.dart +++ b/pkgs/native_toolchain_c/test/clinker/throws_test.dart @@ -2,8 +2,6 @@ // 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:io'; - import 'package:code_assets/code_assets.dart'; import 'package:hooks/hooks.dart'; import 'package:native_toolchain_c/native_toolchain_c.dart'; @@ -11,14 +9,14 @@ import 'package:test/test.dart'; import '../helpers.dart'; -Future main() async { - for (final os in OS.values) { - if (Platform.isLinux) { +void main() { + for (final targetOS in OS.values) { + if (targetOS == OS.linux || targetOS == OS.android) { // Is implemented. continue; } - test('throws on some platforms', () async { + test('throws when targeting $targetOS', () async { final tempUri = await tempDirForTest(); final tempUri2 = await tempDirForTest(); @@ -32,7 +30,7 @@ Future main() async { ..setupLink(assets: [], recordedUsesFile: null) ..addExtension( CodeAssetExtension( - targetOS: os, + targetOS: targetOS, targetArchitecture: Architecture.x64, linkModePreference: LinkModePreference.dynamic, cCompiler: cCompiler, @@ -51,7 +49,7 @@ Future main() async { output: LinkOutputBuilder(), logger: logger, ), - throwsUnsupportedError, + targetOS == OS.fuchsia ? throwsFormatException : throwsUnsupportedError, ); }); } diff --git a/pkgs/native_toolchain_c/test/clinker/treeshake_cross_android_test.dart b/pkgs/native_toolchain_c/test/clinker/treeshake_cross_android_test.dart new file mode 100644 index 000000000..b4d05fa48 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/treeshake_cross_android_test.dart @@ -0,0 +1,30 @@ +// 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. + +import 'package:code_assets/code_assets.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'treeshake_helper.dart'; + +void main() { + final architectures = [ + Architecture.arm, + Architecture.arm64, + Architecture.ia32, + Architecture.x64, + Architecture.riscv64, + ]; + + const targetOS = OS.android; + + for (final apiLevel in [ + flutterAndroidNdkVersionLowestSupported, + flutterAndroidNdkVersionHighestSupported, + ]) { + group('Android API$apiLevel', () { + runTreeshakeTests(targetOS, architectures, androidTargetNdkApi: apiLevel); + }); + } +} diff --git a/pkgs/native_toolchain_c/test/clinker/treeshake_cross_test.dart b/pkgs/native_toolchain_c/test/clinker/treeshake_cross_test.dart index 6dbfcba6b..ea0218b6d 100644 --- a/pkgs/native_toolchain_c/test/clinker/treeshake_cross_test.dart +++ b/pkgs/native_toolchain_c/test/clinker/treeshake_cross_test.dart @@ -14,7 +14,7 @@ import 'package:test/test.dart'; import 'treeshake_helper.dart'; -Future main() async { +void main() { if (!Platform.isLinux) { // Avoid needing status files on Dart SDK CI. return; @@ -28,5 +28,5 @@ Future main() async { Architecture.riscv64, ]..remove(Architecture.current); - await runTests(architectures); + runTreeshakeTests(OS.current, architectures); } diff --git a/pkgs/native_toolchain_c/test/clinker/treeshake_helper.dart b/pkgs/native_toolchain_c/test/clinker/treeshake_helper.dart index 24b5bead4..214deeb69 100644 --- a/pkgs/native_toolchain_c/test/clinker/treeshake_helper.dart +++ b/pkgs/native_toolchain_c/test/clinker/treeshake_helper.dart @@ -2,11 +2,6 @@ // 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. -//TODO(mosuem): Enable for windows and mac. -// See https://github.com/dart-lang/native/issues/1376. -@TestOn('linux') -library; - import 'dart:io'; import 'package:code_assets/code_assets.dart'; @@ -17,13 +12,18 @@ import 'package:test/test.dart'; import '../helpers.dart'; import 'build_testfiles.dart'; -Future runTests(List architectures) async { +void runTreeshakeTests( + OS targetOS, + List architectures, { + int? androidTargetNdkApi, // Must be specified iff targetOS is OS.android. +}) { + assert((targetOS != OS.android) == (androidTargetNdkApi == null)); CLinker linkerManual(List sources) => CLinker.library( name: 'mylibname', assetName: '', sources: sources, linkerOptions: LinkerOptions.manual( - flags: ['--strip-debug', '-u', 'my_other_func'], + flags: ['--strip-debug', '-u,my_other_func'], gcSections: true, linkerScript: packageUri.resolve( 'test/clinker/testfiles/linker/symbols.lds', @@ -43,8 +43,6 @@ Future runTests(List architectures) async { linkerOptions: LinkerOptions.treeshake(symbols: null), ); - const os = OS.linux; - late Map sizes; sizes = {}; for (final architecture in architectures) { @@ -53,65 +51,64 @@ Future runTests(List architectures) async { (name: 'auto', linker: linkerAuto), (name: 'autoEmpty', linker: linkerAutoEmpty), ]) { - test( - 'link test with CLinker ${clinker.name} and target $architecture', - () async { - final tempUri = await tempDirForTest(); - final tempUri2 = await tempDirForTest(); - final testArchive = await buildTestArchive( - tempUri, - tempUri2, - os, - architecture, - ); + test('link test with CLinker ${clinker.name} and target ' + '$architecture for targetOS $targetOS', () async { + final tempUri = await tempDirForTest(); + final tempUri2 = await tempDirForTest(); + final testArchive = await buildTestArchive( + tempUri, + tempUri2, + targetOS, + architecture, + androidTargetNdkApi: androidTargetNdkApi, + ); - final linkInputBuilder = LinkInputBuilder() - ..setupShared( - packageName: 'testpackage', - packageRoot: tempUri, - outputFile: tempUri.resolve('output.json'), - outputDirectoryShared: tempUri2, - ) - ..setupLink(assets: [], recordedUsesFile: null) - ..addExtension( - CodeAssetExtension( - targetOS: os, - targetArchitecture: architecture, - linkModePreference: LinkModePreference.dynamic, - cCompiler: cCompiler, - ), - ); + final linkInputBuilder = LinkInputBuilder() + ..setupShared( + packageName: 'testpackage', + packageRoot: tempUri, + outputFile: tempUri.resolve('output.json'), + outputDirectoryShared: tempUri2, + ) + ..setupLink(assets: [], recordedUsesFile: null) + ..addExtension( + CodeAssetExtension( + targetOS: targetOS, + targetArchitecture: architecture, + linkModePreference: LinkModePreference.dynamic, + cCompiler: cCompiler, + android: androidTargetNdkApi != null + ? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi) + : null, + ), + ); - final linkInput = linkInputBuilder.build(); - final linkOutputBuilder = LinkOutputBuilder(); + final linkInput = linkInputBuilder.build(); + final linkOutputBuilder = LinkOutputBuilder(); - printOnFailure(linkInput.config.code.cCompiler.toString()); - printOnFailure(Platform.environment.keys.toList().toString()); - await clinker - .linker([testArchive.toFilePath()]) - .run(input: linkInput, output: linkOutputBuilder, logger: logger); + printOnFailure(linkInput.config.code.cCompiler.toString()); + printOnFailure(Platform.environment.keys.toList().toString()); + await clinker + .linker([testArchive.toFilePath()]) + .run(input: linkInput, output: linkOutputBuilder, logger: logger); - final linkOutput = linkOutputBuilder.build(); - final asset = linkOutput.assets.code.first; - final filePath = asset.file!.toFilePath(); + final linkOutput = linkOutputBuilder.build(); + final asset = linkOutput.assets.code.first; - final machine = await readelfMachine(filePath); - expect(machine, contains(readElfMachine[architecture])); + await expectMachineArchitecture(asset.file!, architecture); - final symbols = await nmReadSymbols(asset); - if (clinker.linker != linkerAutoEmpty) { - expect(symbols, contains('my_other_func')); - expect(symbols, isNot(contains('my_func'))); - } else { - expect(symbols, contains('my_other_func')); - expect(symbols, contains('my_func')); - } + final symbols = await nmReadSymbols(asset); + if (clinker.linker != linkerAutoEmpty) { + expect(symbols, contains('my_other_func')); + expect(symbols, isNot(contains('my_func'))); + } else { + expect(symbols, contains('my_other_func')); + expect(symbols, contains('my_func')); + } - final du = Process.runSync('du', ['-sb', filePath]).stdout as String; - final sizeInBytes = int.parse(du.split('\t')[0]); - sizes[clinker.name] = sizeInBytes; - }, - ); + final sizeInBytes = await File.fromUri(asset.file!).length(); + sizes[clinker.name] = sizeInBytes; + }); } tearDownAll(() { expect( diff --git a/pkgs/native_toolchain_c/test/clinker/treeshake_test.dart b/pkgs/native_toolchain_c/test/clinker/treeshake_test.dart index 5a8425c4e..86543b14c 100644 --- a/pkgs/native_toolchain_c/test/clinker/treeshake_test.dart +++ b/pkgs/native_toolchain_c/test/clinker/treeshake_test.dart @@ -14,11 +14,11 @@ import 'package:test/test.dart'; import 'treeshake_helper.dart'; -Future main() async { +void main() { if (!Platform.isLinux) { // Avoid needing status files on Dart SDK CI. return; } - await runTests([Architecture.current]); + runTreeshakeTests(OS.current, [Architecture.current]); } diff --git a/pkgs/native_toolchain_c/test/helpers.dart b/pkgs/native_toolchain_c/test/helpers.dart index f571d48b1..3b951b7f2 100644 --- a/pkgs/native_toolchain_c/test/helpers.dart +++ b/pkgs/native_toolchain_c/test/helpers.dart @@ -257,19 +257,6 @@ Future nmReadSymbols(CodeAsset asset) async { return result.stdout; } -Future expectSymbols({ - required CodeAsset asset, - required List symbols, -}) async { - if (Platform.isLinux) { - final nmOutput = await nmReadSymbols(asset); - - expect(nmOutput, stringContainsInOrder(symbols)); - } else { - throw UnimplementedError(); - } -} - Future textSectionAddress(Uri dylib) async { if (Platform.isMacOS) { // Find the line in the objdump output that looks like: @@ -315,3 +302,42 @@ Future expectPageSize(Uri dylib, int pageSize) async { } 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 binaries that run on a +/// given architecture. +const objdumpFileFormat = { + Architecture.arm: 'elf32-littlearm', + Architecture.arm64: 'elf64-littleaarch64', + Architecture.ia32: 'elf32-i386', + Architecture.x64: 'elf64-x86-64', + Architecture.riscv64: 'elf64-littleriscv', +}; + +/// Checks that the provided [libUri] binary has the correct format to be +/// executed on the provided [target] architecture. +/// +/// On Linux, the format of the binary is determined by `readelf`. On MacOS, +/// the `objsdump` tool is used. +Future expectMachineArchitecture(Uri libUri, Architecture target) async { + if (Platform.isLinux) { + final machine = await readelfMachine(libUri.path); + expect(machine, contains(readElfMachine[target])); + } 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(objdumpFileFormat[target])); + } +}