Skip to content

Commit 2239b79

Browse files
committed
[native_toolchain_c] Add linking for macOS
1 parent 5eadfaf commit 2239b79

15 files changed

+172
-116
lines changed

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: 53 additions & 47 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,36 @@ 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.
31-
///
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;
32+
/// Whether to strip debugging symbols from the binary.
33+
final bool stripDebug;
34+
35+
/// The symbols to keep in the resulting binaries.
36+
final List<String>? _symbolsToKeep;
3537

3638
/// Create linking options manually for fine-grained control.
3739
LinkerOptions.manual({
3840
List<String>? flags,
3941
bool? gcSections,
4042
this.linkerScript,
43+
this.stripDebug = true,
44+
Iterable<String>? symbolsToKeep,
4145
}) : _linkerFlags = flags ?? [],
4246
gcSections = gcSections ?? true,
43-
_wholeArchiveSandwich = false;
47+
_symbolsToKeep = symbolsToKeep?.toList(growable: false);
4448

4549
/// Create linking options to tree-shake symbols from the input files.
4650
///
4751
/// The [symbols] specify the symbols which should be kept.
4852
LinkerOptions.treeshake({
4953
Iterable<String>? flags,
5054
required Iterable<String>? symbols,
51-
}) : _linkerFlags = <String>[
52-
...flags ?? [],
53-
'--strip-debug',
54-
if (symbols != null) ...symbols.map((e) => '-u,$e'),
55-
].toList(),
55+
this.stripDebug = true,
56+
}) : _linkerFlags = flags?.toList(growable: false) ?? [],
57+
_symbolsToKeep = symbols?.toList(growable: false),
5658
gcSections = true,
57-
_wholeArchiveSandwich = symbols == null,
5859
linkerScript = _createLinkerScript(symbols);
5960

60-
Iterable<String> _toLinkerSyntax(Tool linker, List<String> flagList) {
61+
Iterable<String> _toLinkerSyntax(Tool linker, Iterable<String> flagList) {
6162
if (linker.isClangLike) {
6263
return flagList.map((e) => '-Wl,$e');
6364
} else if (linker.isLdLike) {
@@ -85,38 +86,43 @@ class LinkerOptions {
8586
}
8687

8788
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,
89+
/// Takes [sourceFiles] and turns it into flags for the compiler driver while
90+
/// considering the current [LinkerOptions].
91+
Iterable<String> sourceFilesToFlags(
92+
Tool tool,
11393
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-
]);
94+
OS targetOS,
95+
) {
96+
final includeAllSymbols = _symbolsToKeep == null;
97+
98+
if (targetOS == OS.macOS) {
99+
return [
100+
if (!includeAllSymbols) ...sourceFiles,
101+
..._toLinkerSyntax(tool, [
102+
if (includeAllSymbols) ...sourceFiles.map((e) => '-force_load,$e'),
103+
..._linkerFlags,
104+
..._symbolsToKeep?.map((symbol) => '-u,_$symbol') ?? [],
105+
if (stripDebug) '-S',
106+
if (gcSections) '-dead_strip',
107+
]),
108+
];
109+
}
110+
111+
final wholeArchiveSandwich =
112+
sourceFiles.any((source) => source.endsWith('.a')) || includeAllSymbols;
113+
114+
return [
115+
if (wholeArchiveSandwich) ..._toLinkerSyntax(tool, ['--whole-archive']),
116+
...sourceFiles,
117+
..._toLinkerSyntax(tool, [
118+
..._linkerFlags,
119+
..._symbolsToKeep?.map((symbol) => '-u,$symbol') ?? [],
120+
if (stripDebug) '--strip-debug',
121+
if (gcSections) '--gc-sections',
122+
if (linkerScript != null)
123+
'--version-script=${linkerScript!.toFilePath()}',
124+
if (wholeArchiveSandwich) '--no-whole-archive',
125+
]),
126+
];
127+
}
122128
}

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/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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ 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 {
2021
assert((targetOS != OS.android) == (androidTargetNdkApi == null));
22+
assert((targetOS != OS.macOS) == (macOSTargetVersion == null));
2123
final test1Uri = packageUri.resolve('test/clinker/testfiles/linker/test1.c');
2224
final test2Uri = packageUri.resolve('test/clinker/testfiles/linker/test2.c');
2325
if (!await File.fromUri(test1Uri).exists() ||
@@ -46,6 +48,9 @@ Future<Uri> buildTestArchive(
4648
android: androidTargetNdkApi != null
4749
? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi)
4850
: null,
51+
macOS: macOSTargetVersion != null
52+
? MacOSCodeConfig(targetVersion: macOSTargetVersion)
53+
: null,
4954
),
5055
);
5156

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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ 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
}) {
2021
assert((targetOS != OS.android) == (androidTargetNdkApi == null));
22+
assert((targetOS != OS.macOS) == (macOSTargetVersion == null));
23+
2124
const name = 'mylibname';
2225

2326
for (final architecture in architectures) {
@@ -31,6 +34,7 @@ void runObjectsTests(
3134
targetOS,
3235
architecture,
3336
androidTargetNdkApi: androidTargetNdkApi,
37+
macOSTargetVersion: macOSTargetVersion,
3438
);
3539

3640
final linkInputBuilder = LinkInputBuilder()
@@ -50,6 +54,9 @@ void runObjectsTests(
5054
android: androidTargetNdkApi != null
5155
? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi)
5256
: null,
57+
macOS: macOSTargetVersion != null
58+
? MacOSCodeConfig(targetVersion: macOSTargetVersion)
59+
: null,
5360
),
5461
);
5562

@@ -70,7 +77,7 @@ void runObjectsTests(
7077
final asset = codeAssets.first;
7178
expect(asset, isA<CodeAsset>());
7279
expect(
73-
await nmReadSymbols(asset),
80+
await nmReadSymbols(asset, targetOS),
7481
stringContainsInOrder(['my_func', 'my_other_func']),
7582
);
7683
});

pkgs/native_toolchain_c/test/clinker/objects_test.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,28 @@
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-
runObjectsTests(OS.current, [Architecture.current]);
24+
runObjectsTests(
25+
OS.current,
26+
[Architecture.current],
27+
macOSTargetVersion: OS.current == OS.macOS ? defaultMacOSVersion : null,
28+
);
2429
}

pkgs/native_toolchain_c/test/clinker/throws_test.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import '../helpers.dart';
1111

1212
void main() {
1313
for (final targetOS in OS.values) {
14-
if (targetOS == OS.linux || targetOS == OS.android) {
14+
if (targetOS == OS.linux ||
15+
targetOS == OS.android ||
16+
targetOS == OS.macOS) {
1517
// Is implemented.
1618
continue;
1719
}

pkgs/native_toolchain_c/test/clinker/treeshake_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 'treeshake_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,

0 commit comments

Comments
 (0)