Skip to content

Commit 92e243f

Browse files
authored
[native_toolchain_c] Add linking for macOS (#2360)
1 parent 5eadfaf commit 92e243f

17 files changed

+199
-119
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.4
2+
3+
* Support linking for MacOS.
4+
15
## 0.16.3
26

37
* Support linking for Android.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class CLinker extends CTool implements Linker {
5353
required LinkOutputBuilder output,
5454
required Logger? logger,
5555
}) async {
56-
const supportedTargetOSs = [OS.linux, OS.android];
56+
const supportedTargetOSs = [OS.linux, OS.android, OS.macOS];
5757
if (!supportedTargetOSs.contains(input.config.code.targetOS)) {
5858
throw UnsupportedError(
5959
'This feature is only supported when targeting '

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

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import 'dart:io';
66

7+
import 'package:code_assets/code_assets.dart';
8+
79
import '../native_toolchain/tool_likeness.dart';
810
import '../tool/tool.dart';
911

@@ -14,7 +16,7 @@ import '../tool/tool.dart';
1416
/// the [LinkerOptions.treeshake] constructor can be used.
1517
class LinkerOptions {
1618
/// The flags to be passed to the linker. As they depend on the linker being
17-
/// invoked, the actual usage is via the [postSourcesFlags] method.
19+
/// invoked, the actual usage is via the [sourceFilesToFlags] method.
1820
final List<String> _linkerFlags;
1921

2022
/// Enable garbage collection of unused input sections.
@@ -27,37 +29,38 @@ class LinkerOptions {
2729
/// See also the `ld` man page at https://linux.die.net/man/1/ld.
2830
final Uri? linkerScript;
2931

30-
/// Whether to include all symbols from the sources.
32+
/// Whether to strip debugging symbols from the binary.
33+
final bool stripDebug;
34+
35+
/// The symbols to keep in the resulting binaries.
3136
///
32-
/// This is achieved by setting the `whole-archive` flag before passing the
33-
/// sources, and the `no-whole-archive` flag after.
34-
final bool _wholeArchiveSandwich;
37+
/// If null all symbols will be kept.
38+
final List<String>? _symbolsToKeep;
3539

3640
/// Create linking options manually for fine-grained control.
3741
LinkerOptions.manual({
3842
List<String>? flags,
3943
bool? gcSections,
4044
this.linkerScript,
45+
this.stripDebug = true,
46+
Iterable<String>? symbolsToKeep,
4147
}) : _linkerFlags = flags ?? [],
4248
gcSections = gcSections ?? true,
43-
_wholeArchiveSandwich = false;
49+
_symbolsToKeep = symbolsToKeep?.toList(growable: false);
4450

4551
/// Create linking options to tree-shake symbols from the input files.
4652
///
4753
/// The [symbols] specify the symbols which should be kept.
4854
LinkerOptions.treeshake({
4955
Iterable<String>? flags,
5056
required Iterable<String>? symbols,
51-
}) : _linkerFlags = <String>[
52-
...flags ?? [],
53-
'--strip-debug',
54-
if (symbols != null) ...symbols.map((e) => '-u,$e'),
55-
].toList(),
57+
this.stripDebug = true,
58+
}) : _linkerFlags = flags?.toList(growable: false) ?? [],
59+
_symbolsToKeep = symbols?.toList(growable: false),
5660
gcSections = true,
57-
_wholeArchiveSandwich = symbols == null,
5861
linkerScript = _createLinkerScript(symbols);
5962

60-
Iterable<String> _toLinkerSyntax(Tool linker, List<String> flagList) {
63+
Iterable<String> _toLinkerSyntax(Tool linker, Iterable<String> flagList) {
6164
if (linker.isClangLike) {
6265
return flagList.map((e) => '-Wl,$e');
6366
} else if (linker.isLdLike) {
@@ -85,38 +88,48 @@ class LinkerOptions {
8588
}
8689

8790
extension LinkerOptionsExt on LinkerOptions {
88-
/// The flags for the specified [linker], which are inserted _before_ the
89-
/// sources.
90-
///
91-
/// This is mainly used for the whole-archive ... no-whole-archive
92-
/// trick, which includes all symbols when linking object files.
93-
///
94-
/// Throws if the [linker] is not supported.
95-
Iterable<String> preSourcesFlags(Tool linker, Iterable<String> sourceFiles) =>
96-
_toLinkerSyntax(
97-
linker,
98-
sourceFiles.any((source) => source.endsWith('.a')) ||
99-
_wholeArchiveSandwich
100-
? ['--whole-archive']
101-
: [],
102-
);
103-
104-
/// The flags for the specified [linker], which are inserted _after_ the
105-
/// sources.
106-
///
107-
/// This is mainly used for the whole-archive ... no-whole-archive
108-
/// trick, which includes all symbols when linking object files.
109-
///
110-
/// Throws if the [linker] is not supported.
111-
Iterable<String> postSourcesFlags(
112-
Tool linker,
91+
/// Takes [sourceFiles] and turns it into flags for the compiler driver while
92+
/// considering the current [LinkerOptions].
93+
Iterable<String> sourceFilesToFlags(
94+
Tool tool,
11395
Iterable<String> sourceFiles,
114-
) => _toLinkerSyntax(linker, [
115-
..._linkerFlags,
116-
if (gcSections) '--gc-sections',
117-
if (linkerScript != null) '--version-script=${linkerScript!.toFilePath()}',
118-
if (sourceFiles.any((source) => source.endsWith('.a')) ||
119-
_wholeArchiveSandwich)
120-
'--no-whole-archive',
121-
]);
96+
OS targetOS,
97+
) {
98+
final includeAllSymbols = _symbolsToKeep == null;
99+
100+
switch (targetOS) {
101+
case OS.macOS:
102+
return [
103+
if (!includeAllSymbols) ...sourceFiles,
104+
..._toLinkerSyntax(tool, [
105+
if (includeAllSymbols) ...sourceFiles.map((e) => '-force_load,$e'),
106+
..._linkerFlags,
107+
..._symbolsToKeep?.map((symbol) => '-u,_$symbol') ?? [],
108+
if (stripDebug) '-S',
109+
if (gcSections) '-dead_strip',
110+
]),
111+
];
112+
113+
case OS.android || OS.linux:
114+
final wholeArchiveSandwich =
115+
sourceFiles.any((source) => source.endsWith('.a')) ||
116+
includeAllSymbols;
117+
return [
118+
if (wholeArchiveSandwich)
119+
..._toLinkerSyntax(tool, ['--whole-archive']),
120+
...sourceFiles,
121+
..._toLinkerSyntax(tool, [
122+
..._linkerFlags,
123+
..._symbolsToKeep?.map((symbol) => '-u,$symbol') ?? [],
124+
if (stripDebug) '--strip-debug',
125+
if (gcSections) '--gc-sections',
126+
if (linkerScript != null)
127+
'--version-script=${linkerScript!.toFilePath()}',
128+
if (wholeArchiveSandwich) '--no-whole-archive',
129+
]),
130+
];
131+
case OS():
132+
throw UnimplementedError();
133+
}
134+
}
122135
}

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,9 @@ class RunCBuilder {
262262
if (dynamicLibrary != null) '-fPIC',
263263
// Using PIC for static libraries allows them to be linked into
264264
// any executable, but it is not necessarily the best option in
265-
// terms of overhead. We would have to know wether the target into
266-
// which the static library is linked is PIC, PIE or neither. Then
267-
// we could use the same option for the static library.
265+
// terms of overhead. We would have to know whether the target
266+
// into which the static library is linked is PIC, PIE or neither.
267+
// Then we could use the same option for the static library.
268268
if (staticLibrary != null) '-fPIC',
269269
if (executable != null) ...[
270270
// Generate position-independent code for executables.
@@ -296,7 +296,6 @@ class RunCBuilder {
296296
],
297297
if (optimizationLevel != OptimizationLevel.unspecified)
298298
optimizationLevel.clangFlag(),
299-
...linkerOptions?.preSourcesFlags(toolInstance.tool, sourceFiles) ?? [],
300299
// Support Android 15 page size by default, can be overridden by
301300
// passing [flags].
302301
if (codeConfig.targetOS == OS.android) '-Wl,-z,max-page-size=16384',
@@ -306,7 +305,14 @@ class RunCBuilder {
306305
for (final include in includes) '-I${include.toFilePath()}',
307306
for (final forcedInclude in forcedIncludes)
308307
'-include${forcedInclude.toFilePath()}',
309-
...sourceFiles,
308+
if (linkerOptions != null)
309+
...linkerOptions!.sourceFilesToFlags(
310+
toolInstance.tool,
311+
sourceFiles,
312+
codeConfig.targetOS,
313+
)
314+
else
315+
...sourceFiles,
310316
if (language == Language.objectiveC) ...[
311317
for (final framework in frameworks) ...['-framework', framework],
312318
],
@@ -322,8 +328,6 @@ class RunCBuilder {
322328
'-o',
323329
outFile!.toFilePath(),
324330
],
325-
...linkerOptions?.postSourcesFlags(toolInstance.tool, sourceFiles) ??
326-
[],
327331
if (executable != null || dynamicLibrary != null) ...[
328332
if (codeConfig.targetOS case OS.android || OS.linux)
329333
// During bundling code assets are all placed in the same directory.

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.3
4+
version: 0.16.4
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ void main() {
4343
linkMode,
4444
optimizationLevel: optimizationLevel,
4545
);
46-
await expectMachineArchitecture(libUri, target);
46+
await expectMachineArchitecture(libUri, target, OS.android);
4747
if (linkMode == DynamicLoadingBundled()) {
4848
await expectPageSize(libUri, 16 * 1024);
4949
}

pkgs/native_toolchain_c/test/clinker/build_testfiles.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ Future<Uri> buildTestArchive(
1616
OS targetOS,
1717
Architecture architecture, {
1818
int? androidTargetNdkApi, // Must be specified iff targetOS is OS.android.
19+
int? macOSTargetVersion, // Must be specified iff targetOS is OS.macos.
1920
}) async {
20-
assert((targetOS != OS.android) == (androidTargetNdkApi == null));
21+
if (targetOS == OS.android) {
22+
ArgumentError.checkNotNull(androidTargetNdkApi, 'androidTargetNdkApi');
23+
}
24+
if (targetOS == OS.macOS) {
25+
ArgumentError.checkNotNull(macOSTargetVersion, 'macOSTargetVersion');
26+
}
27+
2128
final test1Uri = packageUri.resolve('test/clinker/testfiles/linker/test1.c');
2229
final test2Uri = packageUri.resolve('test/clinker/testfiles/linker/test2.c');
2330
if (!await File.fromUri(test1Uri).exists() ||
@@ -46,6 +53,9 @@ Future<Uri> buildTestArchive(
4653
android: androidTargetNdkApi != null
4754
? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi)
4855
: null,
56+
macOS: macOSTargetVersion != null
57+
? MacOSCodeConfig(targetVersion: macOSTargetVersion)
58+
: null,
4959
),
5060
);
5161

pkgs/native_toolchain_c/test/clinker/objects_cross_android_test.dart

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,8 @@ import '../helpers.dart';
99
import 'objects_helper.dart';
1010

1111
void main() {
12-
final architectures = [
13-
Architecture.arm,
14-
Architecture.arm64,
15-
Architecture.ia32,
16-
Architecture.x64,
17-
Architecture.riscv64,
18-
];
19-
2012
const targetOS = OS.android;
13+
final architectures = supportedArchitecturesFor(targetOS);
2114

2215
for (final apiLevel in [
2316
flutterAndroidNdkVersionLowestSupported,

pkgs/native_toolchain_c/test/clinker/objects_cross_test.dart

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,31 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
//TODO(mosuem): Enable for windows and mac.
5+
// TODO(mosuem): Enable for windows.
66
// See https://github.com/dart-lang/native/issues/1376.
7-
@TestOn('linux')
7+
@TestOn('linux || mac-os')
88
library;
99

1010
import 'dart:io';
1111

1212
import 'package:code_assets/code_assets.dart';
1313
import 'package:test/test.dart';
1414

15+
import '../helpers.dart';
1516
import 'objects_helper.dart';
1617

1718
void main() {
18-
if (!Platform.isLinux) {
19+
if (!Platform.isLinux && !Platform.isMacOS) {
1920
// Avoid needing status files on Dart SDK CI.
2021
return;
2122
}
2223

23-
final architectures = [
24-
Architecture.arm,
25-
Architecture.arm64,
26-
Architecture.ia32,
27-
Architecture.x64,
28-
Architecture.riscv64,
29-
]..remove(Architecture.current);
24+
final architectures = supportedArchitecturesFor(OS.current)
25+
..remove(Architecture.current); // See objects_test.dart for current arch.
3026

31-
runObjectsTests(OS.current, architectures);
27+
runObjectsTests(
28+
OS.current,
29+
architectures,
30+
macOSTargetVersion: OS.current == OS.macOS ? defaultMacOSVersion : null,
31+
);
3232
}

pkgs/native_toolchain_c/test/clinker/objects_helper.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ void runObjectsTests(
1616
OS targetOS,
1717
List<Architecture> architectures, {
1818
int? androidTargetNdkApi, // Must be specified iff targetOS is OS.android.
19+
int? macOSTargetVersion, // Must be specified iff targetOS is OS.macos.
1920
}) {
20-
assert((targetOS != OS.android) == (androidTargetNdkApi == null));
21+
if (targetOS == OS.android) {
22+
ArgumentError.checkNotNull(androidTargetNdkApi, 'androidTargetNdkApi');
23+
}
24+
if (targetOS == OS.macOS) {
25+
ArgumentError.checkNotNull(macOSTargetVersion, 'macOSTargetVersion');
26+
}
27+
2128
const name = 'mylibname';
2229

2330
for (final architecture in architectures) {
@@ -31,6 +38,7 @@ void runObjectsTests(
3138
targetOS,
3239
architecture,
3340
androidTargetNdkApi: androidTargetNdkApi,
41+
macOSTargetVersion: macOSTargetVersion,
3442
);
3543

3644
final linkInputBuilder = LinkInputBuilder()
@@ -50,6 +58,9 @@ void runObjectsTests(
5058
android: androidTargetNdkApi != null
5159
? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi)
5260
: null,
61+
macOS: macOSTargetVersion != null
62+
? MacOSCodeConfig(targetVersion: macOSTargetVersion)
63+
: null,
5364
),
5465
);
5566

@@ -70,7 +81,7 @@ void runObjectsTests(
7081
final asset = codeAssets.first;
7182
expect(asset, isA<CodeAsset>());
7283
expect(
73-
await nmReadSymbols(asset),
84+
await nmReadSymbols(asset, targetOS),
7485
stringContainsInOrder(['my_func', 'my_other_func']),
7586
);
7687
});

0 commit comments

Comments
 (0)