Skip to content

Commit 08c98ed

Browse files
authored
[native_toolchain_c] Add linking for Android (#2347)
1 parent e69b7c3 commit 08c98ed

18 files changed

+303
-262
lines changed

pkgs/native_toolchain_c/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.16.3
2+
3+
* Support linking for Android.
4+
15
## 0.16.2
26

37
* Bump the SDK constraint to at least the one from `package:hooks` to fix

pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ class CLinker extends CTool implements Linker {
5353
required LinkOutputBuilder output,
5454
required Logger? logger,
5555
}) async {
56-
if (OS.current != OS.linux || input.config.code.targetOS != OS.linux) {
56+
const supportedTargetOSs = [OS.linux, OS.android];
57+
if (!supportedTargetOSs.contains(input.config.code.targetOS)) {
5758
throw UnsupportedError(
58-
'Currently, only linux is supported for this '
59-
'feature. See also https://github.com/dart-lang/native/issues/1376',
59+
'This feature is only supported when targeting '
60+
'${supportedTargetOSs.join(', ')}. '
61+
'See also https://github.com/dart-lang/native/issues/1376',
6062
);
6163
}
6264
final outDir = input.outputDirectory;

pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -240,81 +240,4 @@ class CompilerResolver {
240240
],
241241
);
242242
}
243-
244-
Future<ToolInstance> resolveLinker() async {
245-
final targetOS = codeConfig.targetOS;
246-
final targetArchitecture = codeConfig.targetArchitecture;
247-
// First, check if the launcher provided a direct path to the compiler.
248-
var result = await _tryLoadLinkerFromInput();
249-
250-
// Then, try to detect on the host machine.
251-
final tool = _selectLinker();
252-
if (tool != null) {
253-
result ??= await _tryLoadToolFromNativeToolchain(tool);
254-
}
255-
256-
if (result != null) {
257-
return result;
258-
}
259-
260-
final errorMessage =
261-
"No linker configured on host '${hostOS}_$hostArchitecture' with "
262-
"target '${targetOS}_$targetArchitecture'.";
263-
logger?.severe(errorMessage);
264-
throw ToolError(errorMessage);
265-
}
266-
267-
Future<ToolInstance?> _tryLoadLinkerFromInput() async {
268-
final inputLdUri = codeConfig.cCompiler?.linker;
269-
if (inputLdUri != null) {
270-
assert(await File.fromUri(inputLdUri).exists());
271-
logger?.finer(
272-
'Using linker ${inputLdUri.toFilePath()} '
273-
'from cCompiler.ld.',
274-
);
275-
final tools = await LinkerRecognizer(inputLdUri).resolve(logger: logger);
276-
return tools.first;
277-
}
278-
logger?.finer('No linker set in cCompiler.ld.');
279-
return null;
280-
}
281-
282-
/// Select the right compiler for cross compiling to the specified target.
283-
Tool? _selectLinker() {
284-
final targetOS = codeConfig.targetOS;
285-
final targetArchitecture = codeConfig.targetArchitecture;
286-
287-
if (targetOS == OS.macOS || targetOS == OS.iOS) return appleLd;
288-
if (targetOS == OS.android) return androidNdkLld;
289-
if (hostOS == OS.linux) {
290-
if (Architecture.current == targetArchitecture) {
291-
return lld;
292-
}
293-
switch (targetArchitecture) {
294-
case Architecture.arm:
295-
return armLinuxGnueabihfLd;
296-
case Architecture.arm64:
297-
return aarch64LinuxGnuLd;
298-
case Architecture.ia32:
299-
return i686LinuxGnuLd;
300-
case Architecture.x64:
301-
return x86_64LinuxGnuLd;
302-
case Architecture.riscv64:
303-
return riscv64LinuxGnuLd;
304-
}
305-
}
306-
307-
if (hostOS == OS.windows) {
308-
switch (targetArchitecture) {
309-
case Architecture.ia32:
310-
return linkIA32;
311-
case Architecture.arm64:
312-
return linkArm64;
313-
case Architecture.x64:
314-
return msvcLink;
315-
}
316-
}
317-
318-
return null;
319-
}
320243
}

pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class LinkerOptions {
5151
}) : _linkerFlags = <String>[
5252
...flags ?? [],
5353
'--strip-debug',
54-
if (symbols != null) ...symbols.expand((e) => ['-u', e]),
54+
if (symbols != null) ...symbols.map((e) => '-u,$e'),
5555
].toList(),
5656
gcSections = true,
5757
_wholeArchiveSandwich = symbols == null,

pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ class RunCBuilder {
9696

9797
Future<Uri> archiver() async => (await _resolver.resolveArchiver()).uri;
9898

99-
Future<ToolInstance> linker() async => await _resolver.resolveLinker();
100-
10199
Future<Uri> iosSdk(IOSSdk iosSdk, {required Logger? logger}) async {
102100
if (iosSdk == IOSSdk.iPhoneOS) {
103101
return (await iPhoneOSSdk.defaultResolver!.resolve(
@@ -119,9 +117,7 @@ class RunCBuilder {
119117
compiler.uri.resolve('../sysroot/');
120118

121119
Future<void> run() async {
122-
final toolInstance_ = linkerOptions != null
123-
? await linker()
124-
: await compiler();
120+
final toolInstance_ = await compiler();
125121
final tool = toolInstance_.tool;
126122
if (tool.isClangLike || tool.isLdLike) {
127123
await runClangLike(tool: toolInstance_);
@@ -333,10 +329,7 @@ class RunCBuilder {
333329
// During bundling code assets are all placed in the same directory.
334330
// Setting this rpath allows the binary to find other code assets
335331
// it is linked against.
336-
if (linkerOptions != null)
337-
'-rpath=\$ORIGIN'
338-
else
339-
'-Wl,-rpath=\$ORIGIN',
332+
'-Wl,-rpath=\$ORIGIN',
340333
for (final directory in libraryDirectories)
341334
'-L${directory.toFilePath()}',
342335
for (final library in libraries) '-l$library',

pkgs/native_toolchain_c/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: native_toolchain_c
22
description: >-
33
A library to invoke the native C compiler installed on the host machine.
4-
version: 0.16.2
4+
version: 0.16.3
55
repository: https://github.com/dart-lang/native/tree/main/pkgs/native_toolchain_c
66

77
topics:

pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'dart:io';
77
import 'package:code_assets/code_assets.dart';
88
import 'package:hooks/hooks.dart';
99
import 'package:native_toolchain_c/native_toolchain_c.dart';
10-
import 'package:native_toolchain_c/src/utils/run_process.dart';
1110
import 'package:test/test.dart';
1211

1312
import '../helpers.dart';
@@ -21,20 +20,6 @@ void main() {
2120
Architecture.riscv64,
2221
];
2322

24-
const objdumpFileFormat = {
25-
Architecture.arm: 'elf32-littlearm',
26-
Architecture.arm64: 'elf64-littleaarch64',
27-
Architecture.ia32: 'elf32-i386',
28-
Architecture.x64: 'elf64-x86-64',
29-
Architecture.riscv64: 'elf64-littleriscv',
30-
};
31-
32-
/// From https://docs.flutter.dev/reference/supported-platforms.
33-
const flutterAndroidNdkVersionLowestSupported = 21;
34-
35-
/// From https://docs.flutter.dev/reference/supported-platforms.
36-
const flutterAndroidNdkVersionHighestSupported = 34;
37-
3823
const optimizationLevels = OptimizationLevel.values;
3924
var selectOptimizationLevel = 0;
4025

@@ -58,21 +43,7 @@ void main() {
5843
linkMode,
5944
optimizationLevel: optimizationLevel,
6045
);
61-
if (Platform.isLinux) {
62-
final machine = await readelfMachine(libUri.path);
63-
expect(machine, contains(readElfMachine[target]));
64-
} else if (Platform.isMacOS) {
65-
final result = await runProcess(
66-
executable: Uri.file('objdump'),
67-
arguments: ['-T', libUri.path],
68-
logger: logger,
69-
);
70-
expect(result.exitCode, 0);
71-
final machine = result.stdout
72-
.split('\n')
73-
.firstWhere((e) => e.contains('file format'));
74-
expect(machine, contains(objdumpFileFormat[target]));
75-
}
46+
await expectMachineArchitecture(libUri, target);
7647
if (linkMode == DynamicLoadingBundled()) {
7748
await expectPageSize(libUri, 16 * 1024);
7849
}

pkgs/native_toolchain_c/test/clinker/build_testfiles.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import '../helpers.dart';
1313
Future<Uri> buildTestArchive(
1414
Uri tempUri,
1515
Uri tempUri2,
16-
OS os,
17-
Architecture architecture,
18-
) async {
16+
OS targetOS,
17+
Architecture architecture, {
18+
int? androidTargetNdkApi, // Must be specified iff targetOS is OS.android.
19+
}) async {
20+
assert((targetOS != OS.android) == (androidTargetNdkApi == null));
1921
final test1Uri = packageUri.resolve('test/clinker/testfiles/linker/test1.c');
2022
final test2Uri = packageUri.resolve('test/clinker/testfiles/linker/test2.c');
2123
if (!await File.fromUri(test1Uri).exists() ||
@@ -27,7 +29,6 @@ Future<Uri> buildTestArchive(
2729
final logMessages = <String>[];
2830
final logger = createCapturingLogger(logMessages);
2931

30-
assert(os == OS.linux); // Setup code input for other OSes.
3132
final buildInputBuilder = BuildInputBuilder()
3233
..setupShared(
3334
packageName: name,
@@ -38,10 +39,13 @@ Future<Uri> buildTestArchive(
3839
..config.setupBuild(linkingEnabled: false)
3940
..addExtension(
4041
CodeAssetExtension(
41-
targetOS: os,
42+
targetOS: targetOS,
4243
targetArchitecture: architecture,
4344
linkModePreference: LinkModePreference.dynamic,
4445
cCompiler: cCompiler,
46+
android: androidTargetNdkApi != null
47+
? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi)
48+
: null,
4549
),
4650
);
4751

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:code_assets/code_assets.dart';
6+
import 'package:test/test.dart';
7+
8+
import '../helpers.dart';
9+
import 'objects_helper.dart';
10+
11+
void main() {
12+
final architectures = [
13+
Architecture.arm,
14+
Architecture.arm64,
15+
Architecture.ia32,
16+
Architecture.x64,
17+
Architecture.riscv64,
18+
];
19+
20+
const targetOS = OS.android;
21+
22+
for (final apiLevel in [
23+
flutterAndroidNdkVersionLowestSupported,
24+
flutterAndroidNdkVersionHighestSupported,
25+
]) {
26+
group('Android API$apiLevel', () {
27+
runObjectsTests(targetOS, architectures, androidTargetNdkApi: apiLevel);
28+
});
29+
}
30+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
//TODO(mosuem): Enable for windows and mac.
6+
// See https://github.com/dart-lang/native/issues/1376.
7+
@TestOn('linux')
8+
library;
9+
10+
import 'dart:io';
11+
12+
import 'package:code_assets/code_assets.dart';
13+
import 'package:test/test.dart';
14+
15+
import 'objects_helper.dart';
16+
17+
void main() {
18+
if (!Platform.isLinux) {
19+
// Avoid needing status files on Dart SDK CI.
20+
return;
21+
}
22+
23+
final architectures = [
24+
Architecture.arm,
25+
Architecture.arm64,
26+
Architecture.ia32,
27+
Architecture.x64,
28+
Architecture.riscv64,
29+
]..remove(Architecture.current);
30+
31+
runObjectsTests(OS.current, architectures);
32+
}

0 commit comments

Comments
 (0)