From 7339ed10f78e9a9b96f167c05c71d4fd130a840d Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 29 Jul 2024 09:55:24 +0200 Subject: [PATCH] [native_toolchain_c] Add linking on Linux (#987) Extend API by providing a `CLinker`, supporting development only on linux for now. Other platforms to be added in future PRs. ---
Contribution guidelines:
- See our [contributor guide](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md) for general expectations for PRs. - Larger or significant changes should be discussed in an issue before creating a PR. - Contributions to our repos should follow the [Dart style guide](https://dart.dev/guides/language/effective-dart) and use `dart format`. - Most changes should add an entry to the changelog and may need to [rev the pubspec package version](https://github.com/dart-lang/sdk/wiki/External-Package-Maintenance#making-a-change). - Changes to packages require [corresponding tests](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md#Testing). Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.
--- .github/workflows/native.yaml | 3 + .../test/build_runner/link_test.dart | 33 +++ .../test_data/manifest.yaml | 12 + .../bin/treeshaking_native_libs.dart | 9 + .../treeshaking_native_libs/ffigen.yaml | 22 ++ .../treeshaking_native_libs/hook/build.dart | 33 +++ .../treeshaking_native_libs/hook/link.dart | 30 +++ .../lib/src/treeshaking_native_libs.dart | 8 + ...haking_native_libs_bindings_generated.dart | 21 ++ .../lib/treeshaking_native_libs.dart | 5 + .../treeshaking_native_libs/pubspec.yaml | 24 ++ .../treeshaking_native_libs/src/native_add.c | 9 + .../treeshaking_native_libs/src/native_add.h | 13 + .../src/native_multiply.c | 9 + .../src/native_multiply.h | 13 + .../test/treeshaking_native_libs_test.dart | 13 + .../lib/native_toolchain_c.dart | 5 +- .../lib/src/cbuilder/cbuilder.dart | 231 ++++-------------- .../lib/src/cbuilder/clinker.dart | 124 ++++++++++ .../lib/src/cbuilder/compiler_resolver.dart | 100 ++++++-- .../lib/src/cbuilder/ctool.dart | 136 +++++++++++ .../lib/src/cbuilder/language.dart | 31 +++ .../lib/src/cbuilder/linker_options.dart | 127 ++++++++++ .../lib/src/cbuilder/linkmode.dart | 15 ++ .../lib/src/cbuilder/output_type.dart | 8 + .../lib/src/cbuilder/run_cbuilder.dart | 85 ++++--- .../lib/src/native_toolchain/msvc.dart | 4 +- .../lib/src/native_toolchain/recognizer.dart | 12 +- .../cbuilder/cbuilder_cross_android_test.dart | 18 +- .../cbuilder_cross_linux_host_test.dart | 19 +- .../test/cbuilder/compiler_resolver_test.dart | 6 +- .../test/clinker/build_testfiles.dart | 57 +++++ .../test/clinker/objects_test.dart | 65 +++++ .../test/clinker/testfiles/linker/symbols.lds | 6 + .../test/clinker/testfiles/linker/test1.c | 6 + .../test/clinker/testfiles/linker/test2.c | 6 + .../test/clinker/throws_test.dart | 45 ++++ .../test/clinker/treeshake_test.dart | 121 +++++++++ pkgs/native_toolchain_c/test/helpers.dart | 28 +++ .../test/native_toolchain/msvc_test.dart | 2 +- .../native_toolchain/recognizer_test.dart | 2 +- 41 files changed, 1230 insertions(+), 286 deletions(-) create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/bin/treeshaking_native_libs.dart create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/ffigen.yaml create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/build.dart create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/link.dart create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs.dart create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs_bindings_generated.dart create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/treeshaking_native_libs.dart create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/pubspec.yaml create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.c create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.h create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.c create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.h create mode 100644 pkgs/native_assets_builder/test_data/treeshaking_native_libs/test/treeshaking_native_libs_test.dart create mode 100644 pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart create mode 100644 pkgs/native_toolchain_c/lib/src/cbuilder/ctool.dart create mode 100644 pkgs/native_toolchain_c/lib/src/cbuilder/language.dart create mode 100644 pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart create mode 100644 pkgs/native_toolchain_c/lib/src/cbuilder/linkmode.dart create mode 100644 pkgs/native_toolchain_c/lib/src/cbuilder/output_type.dart create mode 100644 pkgs/native_toolchain_c/test/clinker/build_testfiles.dart create mode 100644 pkgs/native_toolchain_c/test/clinker/objects_test.dart create mode 100644 pkgs/native_toolchain_c/test/clinker/testfiles/linker/symbols.lds create mode 100644 pkgs/native_toolchain_c/test/clinker/testfiles/linker/test1.c create mode 100644 pkgs/native_toolchain_c/test/clinker/testfiles/linker/test2.c create mode 100644 pkgs/native_toolchain_c/test/clinker/throws_test.dart create mode 100644 pkgs/native_toolchain_c/test/clinker/treeshake_test.dart diff --git a/.github/workflows/native.yaml b/.github/workflows/native.yaml index 7c3563513..964e46464 100644 --- a/.github/workflows/native.yaml +++ b/.github/workflows/native.yaml @@ -108,6 +108,9 @@ jobs: - run: dart pub get -C test_data/add_asset_link/ if: ${{ matrix.package == 'native_assets_builder' }} + - run: dart pub get -C test_data/treeshaking_native_libs/ + if: ${{ matrix.package == 'native_assets_builder' }} + - run: dart pub get -C example/build/native_add_app/ if: ${{ matrix.package == 'native_assets_cli' }} diff --git a/pkgs/native_assets_builder/test/build_runner/link_test.dart b/pkgs/native_assets_builder/test/build_runner/link_test.dart index 70044109c..63aff598c 100644 --- a/pkgs/native_assets_builder/test/build_runner/link_test.dart +++ b/pkgs/native_assets_builder/test/build_runner/link_test.dart @@ -149,6 +149,39 @@ void main() async { ); }); }); + + test('treeshaking assets using CLinker', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('treeshaking_native_libs/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + final buildResult = await build( + packageUri, + logger, + dartExecutable, + linkingEnabled: true, + ); + expect(buildResult.assets.length, 0); + expect(buildResult.assetsForLinking.length, 1); + + final logMessages = []; + final linkResult = await link( + packageUri, + logger, + dartExecutable, + buildResult: buildResult, + capturedLogs: logMessages, + ); + expect(linkResult.assets.length, 1); + expect(linkResult.assets.first, isA()); + }); + }); } Iterable _getNames(List assets) => diff --git a/pkgs/native_assets_builder/test_data/manifest.yaml b/pkgs/native_assets_builder/test_data/manifest.yaml index d0fb0830e..3f5d46a54 100644 --- a/pkgs/native_assets_builder/test_data/manifest.yaml +++ b/pkgs/native_assets_builder/test_data/manifest.yaml @@ -93,6 +93,18 @@ - simple_data_asset/bin/deep_modify_data_asset.dart.debug - some_dev_dep/bin/some_dev_dep.dart - some_dev_dep/pubspec.yaml +- treeshaking_native_libs/pubspec.yaml +- treeshaking_native_libs/lib/treeshaking_native_libs.dart +- treeshaking_native_libs/lib/src/treeshaking_native_libs.dart +- treeshaking_native_libs/lib/src/treeshaking_native_libs_bindings_generated.dart +- treeshaking_native_libs/src/native_add.h +- treeshaking_native_libs/src/native_multiply.h +- treeshaking_native_libs/src/native_add.c +- treeshaking_native_libs/src/native_multiply.c +- treeshaking_native_libs/hook/link.dart +- treeshaking_native_libs/hook/build.dart +- treeshaking_native_libs/bin/treeshaking_native_libs.dart +- treeshaking_native_libs/ffigen.yaml - wrong_build_output_2/hook/build.dart - wrong_build_output_2/pubspec.yaml - wrong_build_output_3/hook/build.dart diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/bin/treeshaking_native_libs.dart b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/bin/treeshaking_native_libs.dart new file mode 100644 index 000000000..bffe25959 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/bin/treeshaking_native_libs.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2024, 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:treeshaking_native_libs/treeshaking_native_libs.dart'; + +void main(List args) { + print(add(5, 3)); +} diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/ffigen.yaml b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/ffigen.yaml new file mode 100644 index 000000000..bbcc0b94b --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/ffigen.yaml @@ -0,0 +1,22 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: NativeCalcBindings +description: | + Bindings for `src/native_add.h` and `src/native_multiply.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: "lib/src/treeshaking_native_libs_bindings_generated.dart" +headers: + entry-points: + - "src/native_add.h" + - "src/native_multiply.h" + include-directives: + - "src/native_add.h" + - "src/native_multiply.h" +preamble: | + // Copyright (c) 2024, 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. +comments: + style: any + length: full +ffi-native: diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/build.dart b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/build.dart new file mode 100644 index 000000000..46f6b1e23 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/build.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2024, 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:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +void main(List arguments) async { + await build(arguments, (config, output) async { + final cbuilder = CBuilder.library( + name: config.packageName + (config.linkingEnabled ? '_static' : ''), + assetName: 'src/${config.packageName}_bindings_generated.dart', + sources: [ + 'src/native_add.c', + 'src/native_multiply.c', + ], + linkModePreference: config.linkingEnabled + ? LinkModePreference.static + : LinkModePreference.dynamic, + ); + await cbuilder.run( + config: config, + output: output, + linkInPackage: config.linkingEnabled ? config.packageName : null, + logger: Logger('') + ..level = Level.ALL + ..onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }), + ); + }); +} diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/link.dart b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/link.dart new file mode 100644 index 000000000..22d7600f6 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/hook/link.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2024, 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:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +void main(List arguments) async { + await link( + arguments, + (config, output) async { + final linker = CLinker.library( + name: config.packageName, + assetName: config.assets.single.id.split('/').skip(1).join('/'), + linkerOptions: LinkerOptions.treeshake(symbols: ['add']), + sources: [config.assets.single.file!.toFilePath()], + ); + await linker.run( + config: config, + output: output, + logger: Logger('') + ..level = Level.ALL + ..onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }), + ); + }, + ); +} diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs.dart b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs.dart new file mode 100644 index 000000000..c6e477b60 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2024, 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 'treeshaking_native_libs_bindings_generated.dart' as bindings; + +int add(int a, int b) => bindings.add(a, b); +int multiply(int a, int b) => bindings.multiply(a, b); diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs_bindings_generated.dart b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs_bindings_generated.dart new file mode 100644 index 000000000..a1e22c95c --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/src/treeshaking_native_libs_bindings_generated.dart @@ -0,0 +1,21 @@ +// 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. + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +@ffi.Native(symbol: 'add') +external int add( + int a, + int b, +); + +@ffi.Native(symbol: 'multiply') +external int multiply( + int a, + int b, +); diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/treeshaking_native_libs.dart b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/treeshaking_native_libs.dart new file mode 100644 index 000000000..5019f562e --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/lib/treeshaking_native_libs.dart @@ -0,0 +1,5 @@ +// Copyright (c) 2024, 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. + +export 'src/treeshaking_native_libs.dart'; diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/pubspec.yaml b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/pubspec.yaml new file mode 100644 index 000000000..a518b905a --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/pubspec.yaml @@ -0,0 +1,24 @@ +name: treeshaking_native_libs +description: Treeshake symbols by name from a native library. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + logging: ^1.1.1 + # native_assets_cli: ^0.7.1 + native_assets_cli: + path: ../../../native_assets_cli/ + # native_toolchain_c: ^0.5.2 + native_toolchain_c: + path: ../../../native_toolchain_c/ + +dev_dependencies: + ffigen: ^8.0.2 + lints: ^3.0.0 + some_dev_dep: + path: ../some_dev_dep/ + test: ^1.23.1 diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.c b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.c new file mode 100644 index 000000000..3c47d5220 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.c @@ -0,0 +1,9 @@ +// Copyright (c) 2024, 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. + +#include "native_add.h" + +int32_t add(int32_t a, int32_t b) { + return a + b; +} diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.h b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.h new file mode 100644 index 000000000..275878fe8 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_add.h @@ -0,0 +1,13 @@ +// Copyright (c) 2024, 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. + +#include + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT int32_t add(int32_t a, int32_t b); diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.c b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.c new file mode 100644 index 000000000..37ad92677 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.c @@ -0,0 +1,9 @@ +// Copyright (c) 2024, 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. + +#include "native_multiply.h" + +MYLIB_EXPORT intptr_t multiply(intptr_t a, intptr_t b) { + return a * b; +} diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.h b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.h new file mode 100644 index 000000000..1c326f4ca --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/src/native_multiply.h @@ -0,0 +1,13 @@ +// Copyright (c) 2024, 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. + +#include + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT intptr_t multiply(intptr_t a, intptr_t b); diff --git a/pkgs/native_assets_builder/test_data/treeshaking_native_libs/test/treeshaking_native_libs_test.dart b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/test/treeshaking_native_libs_test.dart new file mode 100644 index 000000000..575b17ea0 --- /dev/null +++ b/pkgs/native_assets_builder/test_data/treeshaking_native_libs/test/treeshaking_native_libs_test.dart @@ -0,0 +1,13 @@ +// Copyright (c) 2024, 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:test/test.dart'; +import 'package:treeshaking_native_libs/treeshaking_native_libs.dart'; + +void main() { + test('native add test', () { + final result = add(4, 6); + expect(result, equals(10)); + }); +} diff --git a/pkgs/native_toolchain_c/lib/native_toolchain_c.dart b/pkgs/native_toolchain_c/lib/native_toolchain_c.dart index 1b52aaefb..ea6461625 100644 --- a/pkgs/native_toolchain_c/lib/native_toolchain_c.dart +++ b/pkgs/native_toolchain_c/lib/native_toolchain_c.dart @@ -5,5 +5,8 @@ /// A library to invoke the native C compiler installed on the host machine. library; -export 'src/cbuilder/cbuilder.dart'; +export 'src/cbuilder/cbuilder.dart' show CBuilder; +export 'src/cbuilder/clinker.dart' show CLinker, LinkerOptions; +export 'src/cbuilder/language.dart' show Language; +export 'src/cbuilder/output_type.dart' show OutputType; export 'src/utils/env_from_bat.dart'; diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart index 6a9e3737b..d2202b737 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart @@ -8,82 +8,14 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:native_assets_cli/native_assets_cli.dart'; +import 'ctool.dart'; +import 'language.dart'; +import 'linkmode.dart'; +import 'output_type.dart'; import 'run_cbuilder.dart'; -/// A programming language that can be selected for compilation of source files. -/// -/// See [CBuilder.language] for more information. -class Language { - /// The name of the language. - final String name; - - const Language._(this.name); - - static const Language c = Language._('c'); - - static const Language cpp = Language._('c++'); - - static const Language objectiveC = Language._('objective c'); - - /// Known values for [Language]. - static const List values = [ - c, - cpp, - objectiveC, - ]; - - @override - String toString() => name; -} - /// Specification for building an artifact with a C compiler. -class CBuilder implements Builder { - /// What kind of artifact to build. - final _CBuilderType _type; - - /// Name of the library or executable to build. - /// - /// The filename will be decided by [BuildConfig.targetOS] and - /// [OS.libraryFileName] or [OS.executableFileName]. - /// - /// File will be placed in [BuildConfig.outputDirectory]. - final String name; - - /// Asset identifier. - /// - /// Used to output the [BuildOutput.assets]. - /// - /// If omitted, no asset will be added to the build output. - final String? assetName; - - /// Sources to build the library or executable. - /// - /// Resolved against [BuildConfig.packageRoot]. - /// - /// Used to output the [BuildOutput.dependencies]. - final List sources; - - /// Include directories to pass to the compiler. - /// - /// Resolved against [BuildConfig.packageRoot]. - /// - /// Used to output the [BuildOutput.dependencies]. - final List includes; - - /// Frameworks to link. - /// - /// Only effective if [language] is [Language.objectiveC]. - /// - /// Defaults to `['Foundation']`. - /// - /// Not used to output the [BuildOutput.dependencies], frameworks can be - /// mentioned by name if they are available on the system, so the file path - /// is not known. If you're depending on your own frameworks add them to - /// [BuildOutput.dependencies] manually. - final List frameworks; - - static const List _defaultFrameworks = ['Foundation']; - +class CBuilder extends CTool implements Builder { /// The dart files involved in building this artifact. /// /// Resolved against [BuildConfig.packageRoot]. @@ -95,19 +27,6 @@ class CBuilder implements Builder { ) final List dartBuildFiles; - /// TODO(https://github.com/dart-lang/native/issues/54): Move to [BuildConfig] - /// or hide in public API. - @visibleForTesting - final Uri? installName; - - /// Flags to pass to the compiler. - final List flags; - - /// Definitions of preprocessor macros. - /// - /// When the value is `null`, the macro is defined without a value. - final Map defines; - /// Whether to define a macro for the current [BuildMode]. /// /// The macro name is the uppercase name of the build mode and does not have a @@ -127,100 +46,54 @@ class CBuilder implements Builder { /// Defaults to `true`. final bool ndebugDefine; - /// Whether the compiler will emit position independent code. - /// - /// When set to `true`, libraries will be compiled with `-fPIC` and - /// executables with `-fPIE`. Accordingly the corresponding parameter of the - /// [CBuilder.executable] constructor is named `pie`. - /// - /// When set to `null`, the default behavior of the compiler will be used. - /// - /// This option has no effect when building for Windows, where generation of - /// position independent code is not configurable. - /// - /// Defaults to `true` for libraries and `false` for executables. - final bool? pic; - - /// The language standard to use. - /// - /// When set to `null`, the default behavior of the compiler will be used. - final String? std; - - /// The language to compile [sources] as. - /// - /// [cppLinkStdLib] only has an effect when this option is set to - /// [Language.cpp]. - final Language language; - - /// The C++ standard library to link against. - /// - /// This option has no effect when [language] is not set to [Language.cpp] or - /// when compiling for Windows. - /// - /// When set to `null`, the following defaults will be used, based on the - /// target OS: - /// - /// | OS | Library | - /// | :------ | :----------- | - /// | Android | `c++_shared` | - /// | iOS | `c++` | - /// | Linux | `stdc++` | - /// | macOS | `c++` | - /// | Fuchsia | `c++` | - final String? cppLinkStdLib; - - /// If the code asset should be a dynamic or static library. - /// - /// This determines whether to produce a dynamic or static library. If null, - /// the value is instead retrieved from the [BuildConfig]. - final LinkModePreference? linkModePreference; - CBuilder.library({ - required this.name, - required this.assetName, - this.sources = const [], - this.includes = const [], - this.frameworks = _defaultFrameworks, + required super.name, + super.assetName, + super.sources = const [], + super.includes = const [], + super.frameworks = CTool.defaultFrameworks, @Deprecated( 'Newer Dart and Flutter SDKs automatically add the Dart hook ' 'sources as dependencies.', ) this.dartBuildFiles = const [], - @visibleForTesting this.installName, - this.flags = const [], - this.defines = const {}, + @visibleForTesting super.installName, + super.flags = const [], + super.defines = const {}, this.buildModeDefine = true, this.ndebugDefine = true, - this.pic = true, - this.std, - this.language = Language.c, - this.cppLinkStdLib, - this.linkModePreference, - }) : _type = _CBuilderType.library; + super.pic = true, + super.std, + super.language = Language.c, + super.cppLinkStdLib, + super.linkModePreference, + }) : super(type: OutputType.library); CBuilder.executable({ - required this.name, - this.sources = const [], - this.includes = const [], - this.frameworks = _defaultFrameworks, + required super.name, + super.sources = const [], + super.includes = const [], + super.frameworks = CTool.defaultFrameworks, @Deprecated( 'Newer Dart and Flutter SDKs automatically add the Dart hook ' 'sources as dependencies.', ) this.dartBuildFiles = const [], - this.flags = const [], - this.defines = const {}, + super.flags = const [], + super.defines = const {}, this.buildModeDefine = true, this.ndebugDefine = true, bool? pie = false, - this.std, - this.language = Language.c, - this.cppLinkStdLib, - }) : _type = _CBuilderType.executable, - assetName = null, - installName = null, - pic = pie, - linkModePreference = null; + super.std, + super.language = Language.c, + super.cppLinkStdLib, + }) : super( + type: OutputType.executable, + assetName: null, + installName: null, + pic: pie, + linkModePreference: null, + ); /// Runs the C Compiler with on this C build spec. /// @@ -239,7 +112,8 @@ class CBuilder implements Builder { final outDir = config.outputDirectory; final packageRoot = config.packageRoot; await Directory.fromUri(outDir).create(recursive: true); - final linkMode = _linkMode(linkModePreference ?? config.linkModePreference); + final linkMode = + getLinkMode(linkModePreference ?? config.linkModePreference); final libUri = outDir.resolve(config.targetOS.libraryFileName(name, linkMode)); final exeUri = outDir.resolve(config.targetOS.executableFileName(name)); @@ -257,20 +131,20 @@ class CBuilder implements Builder { ]; if (!config.dryRun) { final task = RunCBuilder( - buildConfig: config, + config: config, logger: logger, sources: sources, includes: includes, frameworks: frameworks, - dynamicLibrary: _type == _CBuilderType.library && - linkMode == DynamicLoadingBundled() - ? libUri - : null, - staticLibrary: - _type == _CBuilderType.library && linkMode == StaticLinking() + dynamicLibrary: + type == OutputType.library && linkMode == DynamicLoadingBundled() ? libUri : null, - executable: _type == _CBuilderType.executable ? exeUri : null, + staticLibrary: type == OutputType.library && linkMode == StaticLinking() + ? libUri + : null, + executable: type == OutputType.executable ? exeUri : null, + // ignore: invalid_use_of_visible_for_testing_member installName: installName, flags: flags, defines: { @@ -321,18 +195,3 @@ class CBuilder implements Builder { } } } - -enum _CBuilderType { - executable, - library, -} - -LinkMode _linkMode(LinkModePreference preference) { - if (preference == LinkModePreference.dynamic || - preference == LinkModePreference.preferDynamic) { - return DynamicLoadingBundled(); - } - assert(preference == LinkModePreference.static || - preference == LinkModePreference.preferStatic); - return StaticLinking(); -} diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart new file mode 100644 index 000000000..142ec9c30 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart @@ -0,0 +1,124 @@ +// Copyright (c) 2024, 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:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import 'ctool.dart'; +import 'language.dart'; +import 'linker_options.dart'; +import 'linkmode.dart'; +import 'output_type.dart'; +import 'run_cbuilder.dart'; + +export 'linker_options.dart'; + +/// Specification for linking an artifact with a C linker. +//TODO(mosuem): This is currently only implemented for linux. +// See also https://github.com/dart-lang/native/issues/1376 +class CLinker extends CTool implements Linker { + final LinkerOptions linkerOptions; + + CLinker.library({ + required super.name, + super.assetName, + required this.linkerOptions, + super.sources = const [], + super.includes = const [], + super.frameworks = CTool.defaultFrameworks, + @visibleForTesting super.installName, + super.flags = const [], + super.defines = const {}, + super.pic = true, + super.std, + super.language = Language.c, + super.cppLinkStdLib, + super.linkModePreference, + }) : super(type: OutputType.library); + + /// Runs the C Linker with on this C build spec. + /// + /// Completes with an error if the linking fails. + @override + Future run({ + required LinkConfig config, + required LinkOutput output, + required Logger? logger, + }) async { + if (OS.current != OS.linux || config.targetOS != OS.linux) { + throw UnsupportedError('Currently, only linux is supported for this ' + 'feature. See also https://github.com/dart-lang/native/issues/1376'); + } + final outDir = config.outputDirectory; + final packageRoot = config.packageRoot; + await Directory.fromUri(outDir).create(recursive: true); + final linkMode = + getLinkMode(linkModePreference ?? config.linkModePreference); + final libUri = + outDir.resolve(config.targetOS.libraryFileName(name, linkMode)); + final sources = [ + for (final source in this.sources) + packageRoot.resolveUri(Uri.file(source)), + ]; + final includes = [ + for (final directory in this.includes) + packageRoot.resolveUri(Uri.file(directory)), + ]; + if (!config.dryRun) { + final task = RunCBuilder( + config: config, + linkerOptions: linkerOptions, + logger: logger, + sources: sources, + includes: includes, + frameworks: frameworks, + dynamicLibrary: linkMode == DynamicLoadingBundled() ? libUri : null, + staticLibrary: linkMode == StaticLinking() ? libUri : null, + // ignore: invalid_use_of_visible_for_testing_member + installName: installName, + flags: flags, + defines: defines, + pic: pic, + std: std, + language: language, + cppLinkStdLib: cppLinkStdLib, + ); + await task.run(); + } + + if (assetName != null) { + output.addAssets( + [ + NativeCodeAsset( + package: config.packageName, + name: assetName!, + file: libUri, + linkMode: linkMode, + os: config.targetOS, + architecture: config.dryRun ? null : config.targetArchitecture, + ) + ], + ); + } + if (!config.dryRun) { + final includeFiles = await Stream.fromIterable(includes) + .asyncExpand( + (include) => Directory(include.toFilePath()) + .list(recursive: true) + .where((entry) => entry is File) + .map((file) => file.uri), + ) + .toList(); + + output.addDependencies({ + // Note: We use a Set here to deduplicate the dependencies. + ...sources, + ...includeFiles, + }); + } + } +} 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 cdc8ee0e6..a9bfb41fb 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart @@ -20,13 +20,13 @@ import '../tool/tool_instance.dart'; // TODO(dacoharkes): This should support alternatives. // For example use Clang or MSVC on Windows. class CompilerResolver { - final BuildConfig buildConfig; + final HookConfig hookConfig; final Logger? logger; final OS hostOS; final Architecture hostArchitecture; CompilerResolver({ - required this.buildConfig, + required this.hookConfig, required this.logger, OS? hostOS, // Only visible for testing. Architecture? hostArchitecture, // Only visible for testing. @@ -47,8 +47,8 @@ class CompilerResolver { return result; } - final targetOS = buildConfig.targetOS; - final targetArchitecture = buildConfig.targetArchitecture; + final targetOS = hookConfig.targetOS; + final targetArchitecture = hookConfig.targetArchitecture; final errorMessage = "No tools configured on host '${hostOS}_$hostArchitecture' with target " "'${targetOS}_$targetArchitecture'."; @@ -58,8 +58,8 @@ class CompilerResolver { /// Select the right compiler for cross compiling to the specified target. Tool? _selectCompiler() { - final targetOS = buildConfig.targetOS; - final targetArch = buildConfig.targetArchitecture; + final targetOS = hookConfig.targetOS; + final targetArch = hookConfig.targetArchitecture; // TODO(dacoharkes): Support falling back on other tools. if (targetArch == hostArchitecture && @@ -97,7 +97,7 @@ class CompilerResolver { } Future _tryLoadCompilerFromConfig() async { - final configCcUri = buildConfig.cCompiler.compiler; + final configCcUri = hookConfig.cCompiler.compiler; if (configCcUri != null) { assert(await File.fromUri(configCcUri).exists()); logger?.finer('Using compiler ${configCcUri.toFilePath()} ' @@ -131,8 +131,8 @@ class CompilerResolver { return result; } - final targetOS = buildConfig.targetOS; - final targetArchitecture = buildConfig.targetArchitecture; + final targetOS = hookConfig.targetOS; + final targetArchitecture = hookConfig.targetArchitecture; final errorMessage = "No tools configured on host '${hostOS}_$hostArchitecture' with target " "'${targetOS}_$targetArchitecture'."; @@ -142,8 +142,8 @@ class CompilerResolver { /// Select the right archiver for cross compiling to the specified target. Tool? _selectArchiver() { - final targetOS = buildConfig.targetOS; - final targetArchitecture = buildConfig.targetArchitecture; + final targetOS = hookConfig.targetOS; + final targetArchitecture = hookConfig.targetArchitecture; // TODO(dacoharkes): Support falling back on other tools. if (targetArchitecture == hostArchitecture && @@ -182,7 +182,7 @@ class CompilerResolver { } Future _tryLoadArchiverFromConfig() async { - final configArUri = buildConfig.cCompiler.archiver; + final configArUri = hookConfig.cCompiler.archiver; if (configArUri != null) { assert(await File.fromUri(configArUri).exists()); logger?.finer('Using archiver ${configArUri.toFilePath()} ' @@ -195,7 +195,7 @@ class CompilerResolver { } Future toolchainEnvironmentScript(ToolInstance compiler) async { - final fromConfig = buildConfig.cCompiler.envScript; + final fromConfig = hookConfig.cCompiler.envScript; if (fromConfig != null) { logger?.fine('Using envScript from config: $fromConfig'); return fromConfig; @@ -209,7 +209,7 @@ class CompilerResolver { } List? toolchainEnvironmentScriptArguments() { - final fromConfig = buildConfig.cCompiler.envScriptArgs; + final fromConfig = hookConfig.cCompiler.envScriptArgs; if (fromConfig != null) { logger?.fine('Using envScriptArgs from config: $fromConfig'); return fromConfig; @@ -218,4 +218,76 @@ class CompilerResolver { // vcvars above already has x64 or x86 in the script name. return null; } + + Future resolveLinker() async { + final targetOS = hookConfig.targetOS; + final targetArchitecture = hookConfig.targetArchitecture; + // First, check if the launcher provided a direct path to the compiler. + var result = await _tryLoadLinkerFromConfig(); + + // 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 tools configured on host '${hostOS}_$hostArchitecture' with target " + "'${targetOS}_$targetArchitecture'."; + logger?.severe(errorMessage); + throw ToolError(errorMessage); + } + + Future _tryLoadLinkerFromConfig() async { + final configLdUri = hookConfig.cCompiler.linker; + if (configLdUri != null) { + assert(await File.fromUri(configLdUri).exists()); + logger?.finer('Using linker ${configLdUri.toFilePath()} ' + 'from cCompiler.ld.'); + final tools = await LinkerRecognizer(configLdUri).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 = hookConfig.targetOS; + final targetArchitecture = hookConfig.targetArchitecture; + + if (targetOS == OS.macOS || targetOS == OS.iOS) return appleLd; + if (targetOS == OS.android) return androidNdkLld; + if (hostOS == OS.linux) { + 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/ctool.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/ctool.dart new file mode 100644 index 000000000..c128846dd --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/ctool.dart @@ -0,0 +1,136 @@ +// Copyright (c) 2024, 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:meta/meta.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import 'cbuilder.dart'; +import 'language.dart'; +import 'output_type.dart'; + +abstract class CTool { + /// What kind of artifact to build. + final OutputType type; + + /// Name of the library or executable to linkg. + /// + /// The filename will be decided by [LinkConfig.targetOS] and + /// [OS.libraryFileName] or [OS.executableFileName]. + /// + /// File will be placed in [LinkConfig.outputDirectory]. + final String name; + + /// Asset identifier. + /// + /// Used to output the [LinkConfig.assets]. + /// + /// If omitted, no asset will be added to the build output. + final String? assetName; + + /// Sources to build the library or executable. + /// + /// Resolved against [LinkConfig.packageRoot]. + /// + /// Used to output the [LinkOutput.dependencies]. + final List sources; + + /// Include directories to pass to the linker. + /// + /// Resolved against [LinkConfig.packageRoot]. + /// + /// Used to output the [LinkOutput.dependencies]. + final List includes; + + /// Frameworks to link. + /// + /// Only effective if [language] is [Language.objectiveC]. + /// + /// Defaults to `['Foundation']`. + /// + /// Not used to output the [LinkOutput.dependencies], frameworks can be + /// mentioned by name if they are available on the system, so the file path + /// is not known. If you're depending on your own frameworks add them to + /// [LinkOutput.dependencies] manually. + final List frameworks; + + static const List defaultFrameworks = ['Foundation']; + + /// TODO(https://github.com/dart-lang/native/issues/54): Move to [LinkConfig] + /// or hide in public API. + @visibleForTesting + final Uri? installName; + + /// Flags to pass to the linker. + final List flags; + + /// Definitions of preprocessor macros. + /// + /// When the value is `null`, the macro is defined without a value. + final Map defines; + + /// Whether the linker will emit position independent code. + /// + /// When set to `true`, libraries will be compiled with `-fPIC` and + /// executables with `-fPIE`. Accordingly the corresponding parameter of the + /// [CBuilder.executable] constructor is named `pie`. + /// + /// When set to `null`, the default behavior of the linker will be used. + /// + /// This option has no effect when building for Windows, where generation of + /// position independent code is not configurable. + /// + /// Defaults to `true` for libraries and `false` for executables. + final bool? pic; + + /// The language standard to use. + /// + /// When set to `null`, the default behavior of the linker will be used. + final String? std; + + /// The language to compile [sources] as. + /// + /// [cppLinkStdLib] only has an effect when this option is set to + /// [Language.cpp]. + final Language language; + + /// The C++ standard library to link against. + /// + /// This option has no effect when [language] is not set to [Language.cpp] or + /// when compiling for Windows. + /// + /// When set to `null`, the following defaults will be used, based on the + /// target OS: + /// + /// | OS | Library | + /// | :------ | :----------- | + /// | Android | `c++_shared` | + /// | iOS | `c++` | + /// | Linux | `stdc++` | + /// | macOS | `c++` | + /// | Fuchsia | `c++` | + final String? cppLinkStdLib; + + /// If the code asset should be a dynamic or static library. + /// + /// This determines whether to produce a dynamic or static library. If null, + /// the value is instead retrieved from the [LinkConfig]. + final LinkModePreference? linkModePreference; + + CTool({ + required this.name, + required this.assetName, + required this.sources, + required this.includes, + required this.frameworks, + required this.installName, + required this.flags, + required this.defines, + required this.pic, + required this.std, + required this.language, + required this.cppLinkStdLib, + required this.linkModePreference, + required this.type, + }); +} diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/language.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/language.dart new file mode 100644 index 000000000..b2a0217ce --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/language.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2024, 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 'cbuilder.dart'; + +/// A programming language that can be selected for compilation of source files. +/// +/// See [CBuilder.language] for more information. +class Language { + /// The name of the language. + final String name; + + const Language._(this.name); + + static const Language c = Language._('c'); + + static const Language cpp = Language._('c++'); + + static const Language objectiveC = Language._('objective c'); + + /// Known values for [Language]. + static const List values = [ + c, + cpp, + objectiveC, + ]; + + @override + String toString() => name; +} diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart new file mode 100644 index 000000000..f279c19d9 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart @@ -0,0 +1,127 @@ +// Copyright (c) 2024, 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 '../native_toolchain/clang.dart'; +import '../native_toolchain/gcc.dart'; +import '../tool/tool.dart'; + +/// Options to pass to the linker. +/// +/// These can be manually set via the [LinkerOptions.manual] constructor. +/// Alternatively, if the goal of the linking is to treeshake unused symbols, +/// the [LinkerOptions.treeshake] constructor can be used. +class LinkerOptions { + /// The flags to be passed to the linker. As they depend on the linker being + /// invoked, the actual usage is via the [postSourcesFlags] method. + final List _linkerFlags; + + /// Enable garbage collection of unused input sections. + /// + /// See also the `ld` man page at https://linux.die.net/man/1/ld. + final bool gcSections; + + /// The linker script to be passed via `--version-script`. + /// + /// See also the `ld` man page at https://linux.die.net/man/1/ld. + final Uri? linkerScript; + + /// Whether to include all symbols from the sources. + /// + /// This is achieved by setting the `whole-archive` flag before passing the + /// sources, and the `no-whole-archive` flag after. + final bool _wholeArchiveSandwich; + + /// Create linking options manually for fine-grained control. + LinkerOptions.manual({ + List? flags, + bool? gcSections, + this.linkerScript, + }) : _linkerFlags = flags ?? [], + gcSections = gcSections ?? true, + _wholeArchiveSandwich = false; + + /// Create linking options to tree-shake symbols from the input files. + /// + /// The [symbols] specify the symbols which should be kept. + LinkerOptions.treeshake({ + Iterable? flags, + required Iterable? symbols, + }) : _linkerFlags = { + ...flags ?? [], + '--strip-debug', + if (symbols != null) ...symbols.expand((e) => ['-u', e]), + }.toList(), + gcSections = true, + _wholeArchiveSandwich = symbols == null, + linkerScript = _createLinkerScript(symbols); + + Iterable _toLinkerSyntax(Tool linker, List flagList) { + if (linker == clang) { + return flagList.map((e) => '-Wl,$e'); + } else if (linker == gnuLinker) { + return flagList; + } else { + throw UnsupportedError('Linker flags for $linker are not supported'); + } + } + + static Uri? _createLinkerScript(Iterable? symbols) { + if (symbols == null) return null; + final tempDir = Directory.systemTemp.createTempSync(); + final symbolsFileUri = tempDir.uri.resolve('symbols.lds'); + final symbolsFile = File.fromUri(symbolsFileUri)..createSync(); + symbolsFile.writeAsStringSync(''' +{ + global: + ${symbols.map((e) => '$e;').join('\n ')} + local: + *; +}; +'''); + return symbolsFileUri; + } +} + +extension LinkerOptionsExt on LinkerOptions { + /// The flags for the specified [linker], which are inserted _before_ the + /// sources. + /// + /// This is mainly used for the whole-archive ... no-whole-archive + /// trick, which includes all symbols when linking object files. + /// + /// Throws if the [linker] is not supported. + Iterable preSourcesFlags( + Tool linker, + Iterable sourceFiles, + ) => + _toLinkerSyntax( + linker, + sourceFiles.any((source) => source.endsWith('.a')) || + _wholeArchiveSandwich + ? ['--whole-archive'] + : []); + + /// The flags for the specified [linker], which are inserted _after_ the + /// sources. + /// + /// This is mainly used for the whole-archive ... no-whole-archive + /// trick, which includes all symbols when linking object files. + /// + /// Throws if the [linker] is not supported. + Iterable postSourcesFlags( + Tool linker, + Iterable sourceFiles, + ) => + _toLinkerSyntax(linker, [ + ..._linkerFlags, + if (gcSections) '--gc-sections', + if (linkerScript != null) + '--version-script=${linkerScript!.toFilePath()}', + if (sourceFiles.any((source) => source.endsWith('.a')) || + _wholeArchiveSandwich) + '--no-whole-archive', + ]); +} diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/linkmode.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/linkmode.dart new file mode 100644 index 000000000..d5d08c5be --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/linkmode.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2024, 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:native_assets_cli/native_assets_cli.dart'; + +LinkMode getLinkMode(LinkModePreference preference) { + if (preference == LinkModePreference.dynamic || + preference == LinkModePreference.preferDynamic) { + return DynamicLoadingBundled(); + } + assert(preference == LinkModePreference.static || + preference == LinkModePreference.preferStatic); + return StaticLinking(); +} diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/output_type.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/output_type.dart new file mode 100644 index 000000000..d413afe94 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/output_type.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2024, 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. + +enum OutputType { + executable, + library, +} 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 e2cad2ebb..dd0d0ea3a 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart @@ -15,11 +15,15 @@ import '../native_toolchain/xcode.dart'; import '../tool/tool_instance.dart'; import '../utils/env_from_bat.dart'; import '../utils/run_process.dart'; -import 'cbuilder.dart'; import 'compiler_resolver.dart'; +import 'language.dart'; +import 'linker_options.dart'; class RunCBuilder { - final BuildConfig buildConfig; + /// The options are for linking only, so this will be non-null iff a linker + /// should be run. + final LinkerOptions? linkerOptions; + final HookConfig config; final Logger? logger; final List sources; final List includes; @@ -44,7 +48,8 @@ class RunCBuilder { final String? cppLinkStdLib; RunCBuilder({ - required this.buildConfig, + required this.config, + this.linkerOptions, this.logger, this.sources = const [], this.includes = const [], @@ -59,12 +64,12 @@ class RunCBuilder { this.std, this.language = Language.c, this.cppLinkStdLib, - }) : outDir = buildConfig.outputDirectory, + }) : outDir = config.outputDirectory, assert([executable, dynamicLibrary, staticLibrary] .whereType() .length == 1) { - if (buildConfig.targetOS == OS.windows && cppLinkStdLib != null) { + if (config.targetOS == OS.windows && cppLinkStdLib != null) { throw ArgumentError.value( cppLinkStdLib, 'cppLinkStdLib', @@ -73,13 +78,14 @@ class RunCBuilder { } } - late final _resolver = - CompilerResolver(buildConfig: buildConfig, logger: logger); + late final _resolver = CompilerResolver(hookConfig: config, logger: logger); Future compiler() async => await _resolver.resolveCompiler(); 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(logger: logger)) @@ -104,19 +110,23 @@ class RunCBuilder { compiler.uri.resolve('../sysroot/'); Future run() async { - final compiler_ = await compiler(); - final compilerTool = compiler_.tool; - if (compilerTool == appleClang || - compilerTool == clang || - compilerTool == gcc) { - await runClangLike(compiler: compiler_); + final toolInstance_ = + linkerOptions != null ? await linker() : await compiler(); + final tool = toolInstance_.tool; + if (tool == appleClang || + tool == clang || + tool == gcc || + tool == gnuLinker) { + await runClangLike(tool: toolInstance_); return; + } else if (tool == cl) { + await runCl(tool: toolInstance_); + } else { + throw UnimplementedError('This package does not know how to run $tool.'); } - assert(compilerTool == cl); - await runCl(compiler: compiler_); } - Future runClangLike({required ToolInstance compiler}) async { + Future runClangLike({required ToolInstance tool}) async { final isStaticLib = staticLibrary != null; Uri? archiver_; if (isStaticLib) { @@ -124,8 +134,8 @@ class RunCBuilder { } final IOSSdk? targetIosSdk; - if (buildConfig.targetOS == OS.iOS) { - targetIosSdk = buildConfig.targetIOSSdk; + if (config.targetOS == OS.iOS) { + targetIosSdk = config.targetIOSSdk; } else { targetIosSdk = null; } @@ -134,28 +144,27 @@ class RunCBuilder { // invoking clang. Mimic that behavior here. // See https://github.com/dart-lang/native/issues/171. final int? targetAndroidNdkApi; - if (buildConfig.targetOS == OS.android) { + if (config.targetOS == OS.android) { final minimumApi = - buildConfig.targetArchitecture == Architecture.riscv64 ? 35 : 21; - targetAndroidNdkApi = max(buildConfig.targetAndroidNdkApi!, minimumApi); + config.targetArchitecture == Architecture.riscv64 ? 35 : 21; + targetAndroidNdkApi = max(config.targetAndroidNdkApi!, minimumApi); } else { targetAndroidNdkApi = null; } final targetIOSVersion = - buildConfig.targetOS == OS.iOS ? buildConfig.targetIOSVersion : null; - final targetMacOSVersion = buildConfig.targetOS == OS.macOS - ? buildConfig.targetMacOSVersion - : null; + config.targetOS == OS.iOS ? config.targetIOSVersion : null; + final targetMacOSVersion = + config.targetOS == OS.macOS ? config.targetMacOSVersion : null; - final architecture = buildConfig.targetArchitecture; + final architecture = config.targetArchitecture; final sourceFiles = sources.map((e) => e.toFilePath()).toList(); final objectFiles = []; if (staticLibrary != null) { for (var i = 0; i < sourceFiles.length; i++) { final objectFile = outDir.resolve('out$i.o'); await _compile( - compiler, + tool, architecture, targetAndroidNdkApi, targetIosSdk, @@ -179,7 +188,7 @@ class RunCBuilder { ); } else { await _compile( - compiler, + tool, architecture, targetAndroidNdkApi, targetIosSdk, @@ -204,24 +213,24 @@ class RunCBuilder { await runProcess( executable: compiler.uri, arguments: [ - if (buildConfig.targetOS == OS.android) ...[ + if (config.targetOS == OS.android) ...[ '--target=' '${androidNdkClangTargetFlags[architecture]!}' '${targetAndroidNdkApi!}', '--sysroot=${androidSysroot(compiler).toFilePath()}', ], - if (buildConfig.targetOS == OS.macOS) + if (config.targetOS == OS.macOS) '--target=${appleClangMacosTargetFlags[architecture]!}', - if (buildConfig.targetOS == OS.iOS) + if (config.targetOS == OS.iOS) '--target=${appleClangIosTargetFlags[architecture]![targetIosSdk]!}', if (targetIOSVersion != null) '-mios-version-min=$targetIOSVersion', if (targetMacOSVersion != null) '-mmacos-version-min=$targetMacOSVersion', - if (buildConfig.targetOS == OS.iOS) ...[ + if (config.targetOS == OS.iOS) ...[ '-isysroot', (await iosSdk(targetIosSdk!, logger: logger)).toFilePath(), ], - if (buildConfig.targetOS == OS.macOS) ...[ + if (config.targetOS == OS.macOS) ...[ '-isysroot', (await macosSdk(logger: logger)).toFilePath(), ], @@ -256,8 +265,9 @@ class RunCBuilder { '-x', 'c++', '-l', - cppLinkStdLib ?? defaultCppLinkStdLib[buildConfig.targetOS]! + cppLinkStdLib ?? defaultCppLinkStdLib[config.targetOS]! ], + ...linkerOptions?.preSourcesFlags(compiler.tool, sourceFiles) ?? [], ...flags, for (final MapEntry(key: name, :value) in defines.entries) if (value == null) '-D$name' else '-D$name=$value', @@ -282,6 +292,7 @@ class RunCBuilder { '-o', outFile!.toFilePath(), ], + ...linkerOptions?.postSourcesFlags(compiler.tool, sourceFiles) ?? [], ], logger: logger, captureOutput: false, @@ -289,8 +300,8 @@ class RunCBuilder { ); } - Future runCl({required ToolInstance compiler}) async { - final vcvars = (await _resolver.toolchainEnvironmentScript(compiler))!; + Future runCl({required ToolInstance tool}) async { + final vcvars = (await _resolver.toolchainEnvironmentScript(tool))!; final vcvarsArgs = _resolver.toolchainEnvironmentScriptArguments(); final environment = await environmentFromBatchFile(vcvars, arguments: vcvarsArgs ?? []); @@ -302,7 +313,7 @@ class RunCBuilder { } final result = await runProcess( - executable: compiler.uri, + executable: tool.uri, arguments: [ if (std != null) '/std:$std', if (language == Language.cpp) '/TP', diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart index da527f0b0..e8aef12ed 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart @@ -61,7 +61,7 @@ final Tool msvc = Tool( Tool vcvars(ToolInstance toolInstance) { final tool = toolInstance.tool; - assert(tool == cl || tool == link || tool == lib); + assert(tool == cl || tool == msvcLink || tool == lib); final vcDir = toolInstance.uri.resolve('../../../../../../'); final String fileName; if (toolInstance.uri.toFilePath().contains('\\x86\\')) { @@ -192,7 +192,7 @@ final Tool libArm64 = _msvcTool( resolveVersion: false, ); -final Tool link = _msvcTool( +final Tool msvcLink = _msvcTool( name: 'link', versionArguments: ['/help'], versionExitCode: 1100, diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/recognizer.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/recognizer.dart index 4080f5e93..6c8b430e9 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/recognizer.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/recognizer.dart @@ -11,7 +11,7 @@ import '../tool/tool_resolver.dart'; import 'apple_clang.dart'; import 'clang.dart'; import 'gcc.dart'; -import 'msvc.dart' as msvc; +import 'msvc.dart'; class CompilerRecognizer implements ToolResolver { final Uri uri; @@ -35,7 +35,7 @@ class CompilerRecognizer implements ToolResolver { tool = clang; } } else if (filePath.endsWith('cl.exe')) { - tool = msvc.cl; + tool = cl; } if (tool != null) { @@ -46,7 +46,7 @@ class CompilerRecognizer implements ToolResolver { toolInstance, logger: logger, arguments: [ - if (tool != msvc.cl) '--version', + if (tool != cl) '--version', ], ), ]; @@ -75,7 +75,7 @@ class LinkerRecognizer implements ToolResolver { } else if (filePath.endsWith(os.executableFileName('ld'))) { tool = appleLd; } else if (filePath.endsWith('link.exe')) { - tool = msvc.link; + tool = msvcLink; } if (tool != null) { @@ -89,7 +89,7 @@ class LinkerRecognizer implements ToolResolver { ), ]; } - if (tool == msvc.link) { + if (tool == msvcLink) { return [ await CliVersionResolver.lookupVersion( toolInstance, @@ -125,7 +125,7 @@ class ArchiverRecognizer implements ToolResolver { } else if (filePath.endsWith(os.executableFileName('ar'))) { tool = appleAr; } else if (filePath.endsWith('lib.exe')) { - tool = msvc.lib; + tool = lib; } if (tool != null) { 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 2972af5f3..a5558f939 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 @@ -21,14 +21,6 @@ void main() { // Architecture.riscv64, ]; - const readElfMachine = { - Architecture.arm: 'ARM', - Architecture.arm64: 'AArch64', - Architecture.ia32: 'Intel 80386', - Architecture.x64: 'Advanced Micro Devices X86-64', - Architecture.riscv64: 'RISC-V', - }; - const objdumpFileFormat = { Architecture.arm: 'elf32-littlearm', Architecture.arm64: 'elf64-littleaarch64', @@ -63,15 +55,7 @@ void main() { linkMode, ); if (Platform.isLinux) { - final result = await runProcess( - executable: Uri.file('readelf'), - arguments: ['-h', libUri.path], - logger: logger, - ); - expect(result.exitCode, 0); - final machine = result.stdout - .split('\n') - .firstWhere((e) => e.contains('Machine:')); + final machine = await readelfMachine(libUri.path); expect(machine, contains(readElfMachine[target])); } else if (Platform.isMacOS) { final result = await runProcess( diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart index 1bb3eb510..389c4f5ba 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart @@ -9,7 +9,6 @@ import 'dart:io'; import 'package:native_assets_cli/native_assets_cli.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'; @@ -28,14 +27,6 @@ void main() { Architecture.riscv64, ]; - const readElfMachine = { - Architecture.arm: 'ARM', - Architecture.arm64: 'AArch64', - Architecture.ia32: 'Intel 80386', - Architecture.x64: 'Advanced Micro Devices X86-64', - Architecture.riscv64: 'RISC-V', - }; - for (final linkMode in [DynamicLoadingBundled(), StaticLinking()]) { for (final target in targets) { test('CBuilder $linkMode library $target', () async { @@ -71,16 +62,8 @@ void main() { final libUri = tempUri.resolve(OS.linux.libraryFileName(name, linkMode)); - final result = await runProcess( - executable: Uri.file('readelf'), - arguments: ['-h', libUri.path], - logger: logger, - ); - expect(result.exitCode, 0); - final machine = - result.stdout.split('\n').firstWhere((e) => e.contains('Machine:')); + final machine = await readelfMachine(libUri.path); expect(machine, contains(readElfMachine[target])); - expect(result.exitCode, 0); }); } } diff --git a/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart index a30a94c1d..6c81c9455 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart @@ -34,7 +34,7 @@ void main() { ].first.uri; final ld = [ ...await appleLd.defaultResolver!.resolve(logger: logger), - ...await msvc.link.defaultResolver!.resolve(logger: logger), + ...await msvc.msvcLink.defaultResolver!.resolve(logger: logger), ...await lld.defaultResolver!.resolve(logger: logger), ].first.uri; final envScript = [ @@ -56,7 +56,7 @@ void main() { ), linkingEnabled: false, ); - final resolver = CompilerResolver(buildConfig: buildConfig, logger: logger); + final resolver = CompilerResolver(hookConfig: buildConfig, logger: logger); final compiler = await resolver.resolveCompiler(); final archiver = await resolver.resolveArchiver(); expect(compiler.uri, buildConfig.cCompiler.compiler); @@ -76,7 +76,7 @@ void main() { linkingEnabled: false, ); final resolver = CompilerResolver( - buildConfig: buildConfig, + hookConfig: buildConfig, logger: logger, hostOS: OS.android, // This is never a host. hostArchitecture: Architecture.arm64, // This is never a host. diff --git a/pkgs/native_toolchain_c/test/clinker/build_testfiles.dart b/pkgs/native_toolchain_c/test/clinker/build_testfiles.dart new file mode 100644 index 000000000..1e21cf3a4 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/build_testfiles.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2024, 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:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +import '../helpers.dart'; + +Future buildTestArchive( + Uri tempUri, + OS os, + Architecture architecture, +) async { + 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() || + !await File.fromUri(test2Uri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'static_test'; + + final logMessages = []; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig.build( + outputDirectory: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: architecture, + targetOS: os, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + compiler: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + linkingEnabled: false, + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.library( + name: name, + assetName: '', + sources: [test1Uri.toFilePath(), test2Uri.toFilePath()], + linkModePreference: LinkModePreference.static, + ); + await cbuilder.run( + config: buildConfig, + output: buildOutput, + logger: logger, + ); + + return buildOutput.assets.first.file!; +} diff --git a/pkgs/native_toolchain_c/test/clinker/objects_test.dart b/pkgs/native_toolchain_c/test/clinker/objects_test.dart new file mode 100644 index 000000000..f71bfe359 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/objects_test.dart @@ -0,0 +1,65 @@ +// Copyright (c) 2024, 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:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'build_testfiles.dart'; + +Future main() async { + if (!Platform.isLinux) { + // Avoid needing status files on Dart SDK CI. + return; + } + const architecture = Architecture.x64; + const os = OS.linux; + const name = 'mylibname'; + + test('link two objects', () async { + final linkOutput = LinkOutput(); + final tempUri = await tempDirForTest(); + + final uri = await buildTestArchive(tempUri, os, architecture); + + final linkConfig = LinkConfig.build( + outputDirectory: tempUri, + packageName: 'testpackage', + packageRoot: tempUri, + targetArchitecture: architecture, + targetOS: os, + buildMode: BuildMode.debug, + linkModePreference: LinkModePreference.dynamic, + assets: []); + await CLinker.library( + name: name, + assetName: '', + linkerOptions: LinkerOptions.manual(gcSections: false), + sources: [uri.toFilePath()], + ).run( + config: linkConfig, + output: linkOutput, + logger: logger, + ); + + expect(linkOutput.assets, hasLength(1)); + final asset = linkOutput.assets.first; + expect(asset, isA()); + final file = (asset as NativeCodeAsset).file; + expect(file, isNotNull, reason: 'Asset $asset has a file'); + final filePath = file!.toFilePath(); + expect(filePath, endsWith(os.dylibFileName(name))); + final readelf = await readelfSymbols(filePath); + expect(readelf, contains('my_other_func')); + expect(readelf, contains('my_func')); + }); +} diff --git a/pkgs/native_toolchain_c/test/clinker/testfiles/linker/symbols.lds b/pkgs/native_toolchain_c/test/clinker/testfiles/linker/symbols.lds new file mode 100644 index 000000000..d3ff23f54 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/testfiles/linker/symbols.lds @@ -0,0 +1,6 @@ +{ + global: + my_other_func; + local: + *; +}; diff --git a/pkgs/native_toolchain_c/test/clinker/testfiles/linker/test1.c b/pkgs/native_toolchain_c/test/clinker/testfiles/linker/test1.c new file mode 100644 index 000000000..a7132d887 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/testfiles/linker/test1.c @@ -0,0 +1,6 @@ +#include + +void my_func() +{ + printf("42"); +} diff --git a/pkgs/native_toolchain_c/test/clinker/testfiles/linker/test2.c b/pkgs/native_toolchain_c/test/clinker/testfiles/linker/test2.c new file mode 100644 index 000000000..1348214d2 --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/testfiles/linker/test2.c @@ -0,0 +1,6 @@ +#include + +void my_other_func() +{ + printf("42+1"); +} diff --git a/pkgs/native_toolchain_c/test/clinker/throws_test.dart b/pkgs/native_toolchain_c/test/clinker/throws_test.dart new file mode 100644 index 000000000..01c1438cb --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/throws_test.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2024, 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:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +Future main() async { + for (final os in OS.values) { + test( + 'throws on some platforms', + () async { + final tempUri = await tempDirForTest(); + + final cLinker = CLinker.library( + name: 'mylibname', + linkerOptions: LinkerOptions.manual(), + ); + await expectLater( + () => cLinker.run( + config: LinkConfig.build( + outputDirectory: tempUri, + packageName: 'testpackage', + packageRoot: tempUri, + targetArchitecture: Architecture.x64, + targetOS: os, + buildMode: BuildMode.debug, + linkModePreference: LinkModePreference.dynamic, + assets: [], + ), + output: LinkOutput(), + logger: logger, + ), + throwsUnsupportedError, + ); + }, + onPlatform: { + 'linux': const Skip('Is implemented'), + }, + ); + } +} diff --git a/pkgs/native_toolchain_c/test/clinker/treeshake_test.dart b/pkgs/native_toolchain_c/test/clinker/treeshake_test.dart new file mode 100644 index 000000000..a7c7d7a5b --- /dev/null +++ b/pkgs/native_toolchain_c/test/clinker/treeshake_test.dart @@ -0,0 +1,121 @@ +// Copyright (c) 2024, 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:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'build_testfiles.dart'; + +Future main() async { + if (!Platform.isLinux) { + // Avoid needing status files on Dart SDK CI. + return; + } + + CLinker linkerManual(List sources) => CLinker.library( + name: 'mylibname', + assetName: '', + sources: sources, + linkerOptions: LinkerOptions.manual( + flags: ['--strip-debug', '-u', 'my_other_func'], + gcSections: true, + linkerScript: Uri.file('test/clinker/testfiles/linker/symbols.lds'), + ), + ); + CLinker linkerAuto(List sources) => CLinker.library( + name: 'mylibname', + assetName: '', + sources: sources, + linkerOptions: LinkerOptions.treeshake(symbols: ['my_other_func']), + ); + CLinker linkerAutoEmpty(List sources) => CLinker.library( + name: 'mylibname', + assetName: '', + sources: sources, + linkerOptions: LinkerOptions.treeshake(symbols: null), + ); + + const os = OS.linux; + const architectures = [ + Architecture.arm, + Architecture.arm64, + Architecture.ia32, + Architecture.x64, + Architecture.riscv64, + ]; + + late Map sizes; + sizes = {}; + for (final architecture in architectures) { + for (final clinker in [ + (name: 'manual', linker: linkerManual), + (name: 'auto', linker: linkerAuto), + (name: 'autoEmpty', linker: linkerAutoEmpty), + ]) { + test('link test with CLinker ${clinker.name} and target $architecture', + () async { + final tempUri = await tempDirForTest(); + final testArchive = await buildTestArchive(tempUri, os, architecture); + + final linkOutput = LinkOutput(); + + final config = LinkConfig.build( + outputDirectory: tempUri, + packageName: 'testpackage', + packageRoot: tempUri, + targetArchitecture: architecture, + targetOS: os, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + assets: [], + ); + await clinker.linker([testArchive.toFilePath()]).run( + config: config, + output: linkOutput, + logger: logger, + ); + final filePath = linkOutput.assets.first.file!.toFilePath(); + + final machine = await readelfMachine(filePath); + expect(machine, contains(readElfMachine[architecture])); + + final symbols = await readelfSymbols(filePath); + if (clinker.linker != linkerAutoEmpty) { + expect(symbols, matches(r'[0-9]+\smy_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; + }); + } + tearDownAll( + () { + expect( + sizes['manual'], + lessThan(sizes['autoEmpty']!), + reason: 'Tree-shaking reduces size', + ); + expect( + sizes['auto'], + lessThan(sizes['autoEmpty']!), + reason: 'Tree-shaking reduces size', + ); + }, + ); + } +} diff --git a/pkgs/native_toolchain_c/test/helpers.dart b/pkgs/native_toolchain_c/test/helpers.dart index 612c2f087..31b0d458b 100644 --- a/pkgs/native_toolchain_c/test/helpers.dart +++ b/pkgs/native_toolchain_c/test/helpers.dart @@ -7,6 +7,7 @@ import 'dart:ffi'; import 'dart:io'; import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; import 'package:native_toolchain_c/src/native_toolchain/apple_clang.dart'; import 'package:native_toolchain_c/src/utils/run_process.dart'; import 'package:test/test.dart'; @@ -184,3 +185,30 @@ DynamicLibrary openDynamicLibraryForTest(String path) { extension UnescapePath on String { String unescape() => replaceAll('\\', '/'); } + +Future readelfSymbols(String filePath) async => + readelf(filePath, 'WCs'); + +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; +} diff --git a/pkgs/native_toolchain_c/test/native_toolchain/msvc_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/msvc_test.dart index 59a584cce..60dbe3c10 100644 --- a/pkgs/native_toolchain_c/test/native_toolchain/msvc_test.dart +++ b/pkgs/native_toolchain_c/test/native_toolchain/msvc_test.dart @@ -69,7 +69,7 @@ void main() { }); test('link', () async { - final instances = await link.defaultResolver!.resolve(logger: logger); + final instances = await msvcLink.defaultResolver!.resolve(logger: logger); expect(instances.isNotEmpty, true); }); diff --git a/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart index 0f18d39bb..c8b96fa57 100644 --- a/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart +++ b/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart @@ -36,7 +36,7 @@ void main() async { RecognizerTest(i686LinuxGnuGccAr, ArchiverRecognizer.new), RecognizerTest(i686LinuxGnuLd, LinkerRecognizer.new), RecognizerTest(lib, ArchiverRecognizer.new), - RecognizerTest(link, LinkerRecognizer.new), + RecognizerTest(msvcLink, LinkerRecognizer.new), RecognizerTest(lld, LinkerRecognizer.new), RecognizerTest(llvmAr, ArchiverRecognizer.new), RecognizerTest(riscv64LinuxGnuGcc, CompilerRecognizer.new),