Merge package:ffigen into dart-lang/native
diff --git a/.github/ISSUE_TEMPLATE/native_assets_builder.md b/.github/ISSUE_TEMPLATE/native_assets_builder.md new file mode 100644 index 0000000..cc51ab0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/native_assets_builder.md
@@ -0,0 +1,5 @@ +--- +name: "package:native_assets_builder" +about: "Create a bug or file a feature request against package:native_assets_builder." +labels: "package:native_assets_builder" +--- \ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/native_assets_cli.md b/.github/ISSUE_TEMPLATE/native_assets_cli.md new file mode 100644 index 0000000..d6a22a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/native_assets_cli.md
@@ -0,0 +1,5 @@ +--- +name: "package:native_assets_cli" +about: "Create a bug or file a feature request against package:native_assets_cli." +labels: "package:native_assets_cli" +--- \ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/native_toolchain_c.md b/.github/ISSUE_TEMPLATE/native_toolchain_c.md new file mode 100644 index 0000000..161bec6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/native_toolchain_c.md
@@ -0,0 +1,5 @@ +--- +name: "package:native_toolchain_c" +about: "Create a bug or file a feature request against package:native_toolchain_c." +labels: "package:native_toolchain_c" +--- \ No newline at end of file
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..439e796 --- /dev/null +++ b/.github/dependabot.yaml
@@ -0,0 +1,10 @@ +# Dependabot configuration file. +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - autosubmit
diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..ca96162 --- /dev/null +++ b/.github/labeler.yml
@@ -0,0 +1,13 @@ +# This configures the .github/workflows/pull_request_label.yml workflow. + +'type-infra': + - .github/** + +'package:native_assets_builder': + - pkgs/native_assets_builder/**/* + +'package:native_assets_cli': + - pkgs/native_assets_cli/**/* + +'package:native_toolchain_c': + - pkgs/native_toolchain_c/**/*
diff --git a/.github/workflows/native.yaml b/.github/workflows/native.yaml new file mode 100644 index 0000000..2da22ce --- /dev/null +++ b/.github/workflows/native.yaml
@@ -0,0 +1,139 @@ +# CI for the native_* packages. +# +# Combined into a single workflow so that deps are configured and installed once. + +name: native +permissions: read-all + +on: + pull_request: + branches: [main] + paths: + - ".github/workflows/native.yaml" + - "pkgs/native_assets_builder/**" + - "pkgs/native_assets_cli/**" + - "pkgs/native_toolchain_c/**" + push: + branches: [main] + paths: + - ".github/workflows/native.yaml" + - "pkgs/native_assets_builder/**" + - "pkgs/native_assets_cli/**" + - "pkgs/native_toolchain_c/**" + schedule: + - cron: "0 0 * * 0" # weekly + +jobs: + build: + strategy: + matrix: + os: [ubuntu, macos, windows] + sdk: [stable, dev] + package: [native_assets_builder, native_assets_cli, native_toolchain_c] + # Breaking changes temporarily break the example run on the Dart SDK until native_assets_builder is rolled into the Dart SDK dev build. + breaking-change: [true] + exclude: + # Only run analyze against dev on one host. + - os: macos + sdk: dev + - os: windows + sdk: dev + + runs-on: ${{ matrix.os }}-latest + + defaults: + run: + working-directory: pkgs/${{ matrix.package }} + + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + with: + sdk: ${{ matrix.sdk }} + + - uses: nttld/setup-ndk@3354316c3285ea90da09d047280dd79d00d5a37a + with: + ndk-version: r26b + if: ${{ matrix.sdk == 'stable' }} + + - run: dart pub get + + - run: dart pub get -C test/data/dart_app/ + if: ${{ matrix.package == 'native_assets_builder' }} + + - run: dart pub get -C test/data/native_add/ + if: ${{ matrix.package == 'native_assets_builder' }} + + - run: dart pub get -C test/data/native_add_add_source/ + if: ${{ matrix.package == 'native_assets_builder' }} + + - run: dart pub get -C test/data/native_subtract/ + if: ${{ matrix.package == 'native_assets_builder' }} + + - run: dart pub get -C test/data/package_reading_metadata/ + if: ${{ matrix.package == 'native_assets_builder' }} + + - run: dart pub get -C test/data/package_with_metadata/ + if: ${{ matrix.package == 'native_assets_builder' }} + + - run: dart pub get -C example/native_add_app/ + if: ${{ matrix.package == 'native_assets_cli' }} + + - run: dart pub get -C example/native_add_library/ + if: ${{ matrix.package == 'native_assets_cli' }} + + - run: dart analyze --fatal-infos + # Run on dev to ensure we're not depending on deprecated SDK things. + + - run: dart format --output=none --set-exit-if-changed . + if: ${{ matrix.sdk == 'stable' }} + + - name: Install native toolchains + run: sudo apt-get update && sudo apt-get install clang-15 gcc-i686-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-riscv64-linux-gnu + if: ${{ matrix.sdk == 'stable' && matrix.os == 'ubuntu' }} + + - run: dart test + if: ${{ matrix.sdk == 'stable' }} + + - run: dart --enable-experiment=native-assets test + working-directory: pkgs/${{ matrix.package }}/example/native_add_app/ + if: ${{ matrix.package == 'native_assets_cli' && matrix.sdk == 'dev' && !matrix.breaking-change }} + + - run: dart --enable-experiment=native-assets run + working-directory: pkgs/${{ matrix.package }}/example/native_add_app/ + if: ${{ matrix.package == 'native_assets_cli' && matrix.sdk == 'dev' && !matrix.breaking-change }} + + - run: dart --enable-experiment=native-assets build bin/native_add_app.dart + working-directory: pkgs/${{ matrix.package }}/example/native_add_app/ + if: ${{ matrix.package == 'native_assets_cli' && matrix.sdk == 'dev' && !matrix.breaking-change }} + + - run: ./native_add_app.exe + working-directory: pkgs/${{ matrix.package }}/example/native_add_app/bin/native_add_app/ + if: ${{ matrix.package == 'native_assets_cli' && matrix.sdk == 'dev' && !matrix.breaking-change }} + + - name: Install coverage + run: dart pub global activate coverage + if: ${{ matrix.sdk == 'stable' }} + + - name: Collect coverage + run: dart pub global run coverage:test_with_coverage + if: ${{ matrix.sdk == 'stable' }} + + - name: Upload coverage + uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 + with: + flag-name: ${{ matrix.package }}-${{ matrix.os }} + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel: true + if: ${{ matrix.sdk == 'stable' }} + + coverage-finished: + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Upload coverage + uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true
diff --git a/.github/workflows/native_toolchain_c.yaml b/.github/workflows/native_toolchain_c.yaml new file mode 100644 index 0000000..ceb668b --- /dev/null +++ b/.github/workflows/native_toolchain_c.yaml
@@ -0,0 +1,60 @@ +# Workflow that runs relevant tests with the clang from the Dart SDK. + +name: native_toolchain_c +permissions: read-all + +on: + pull_request: + branches: [main] + paths: + - ".github/workflows/native_toolchain_c.yaml" + - "pkgs/native_toolchain_c/**" + push: + branches: [main] + paths: + - ".github/workflows/native_toolchain_c.yaml" + - "pkgs/native_toolchain_c/**" + schedule: + - cron: "0 0 * * 0" # weekly + +jobs: + dart-sdk-clang: + strategy: + matrix: + os: [ubuntu] + sdk: [stable] + package: [native_toolchain_c] + + runs-on: ${{ matrix.os }}-latest + + defaults: + run: + working-directory: pkgs/${{ matrix.package }} + + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + with: + sdk: ${{ matrix.sdk }} + + - uses: nttld/setup-ndk@3354316c3285ea90da09d047280dd79d00d5a37a + with: + ndk-version: r26b + if: ${{ matrix.sdk == 'stable' }} + + - run: dart pub get + + - name: Install native toolchains + run: sudo apt-get update && sudo apt-get install gcc-i686-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-riscv64-linux-gnu + if: ${{ matrix.sdk == 'stable' && matrix.os == 'ubuntu' }} + + - run: git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git + - run: echo "$PWD/depot_tools" >> $GITHUB_PATH + - run: mkdir dart-sdk + - run: cd dart-sdk && fetch --no-history dart + - run: echo "./dart-sdk/sdk/buildtools/linux-x64/clang/bin" >> $GITHUB_PATH + - run: clang --version + + - run: dart test + if: ${{ matrix.sdk == 'stable' }}
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 0000000..8e5ed57 --- /dev/null +++ b/.github/workflows/no-response.yml
@@ -0,0 +1,37 @@ +# A workflow to close issues where the author hasn't responded to a request for +# more information; see https://github.com/actions/stale. + +name: No Response + +# Run as a daily cron. +on: + schedule: + # Every day at 8am + - cron: '0 8 * * *' + +# All permissions not specified are set to 'none'. +permissions: + issues: write + pull-requests: write + +jobs: + no-response: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'dart-lang' }} + steps: + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 + with: + # Don't automatically mark inactive issues+PRs as stale. + days-before-stale: -1 + # Close needs-info issues and PRs after 14 days of inactivity. + days-before-close: 14 + stale-issue-label: "needs-info" + close-issue-message: > + Without additional information we're not able to resolve this issue. + Feel free to add more info or respond to any questions above and we + can reopen the case. Thanks for your contribution! + stale-pr-label: "needs-info" + close-pr-message: > + Without additional information we're not able to resolve this PR. + Feel free to add more info or respond to any questions above. + Thanks for your contribution!
diff --git a/.github/workflows/post_summaries.yaml b/.github/workflows/post_summaries.yaml new file mode 100644 index 0000000..a475092 --- /dev/null +++ b/.github/workflows/post_summaries.yaml
@@ -0,0 +1,16 @@ +name: Comment on the pull request + +on: + # Trigger this workflow after the Health workflow completes. This workflow will have permissions to + # do things like create comments on the PR, even if the original workflow couldn't. + workflow_run: + workflows: + - Publish + types: + - completed + +jobs: + upload: + uses: dart-lang/ecosystem/.github/workflows/post_summaries.yaml@main + permissions: + pull-requests: write
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..4bac353 --- /dev/null +++ b/.github/workflows/publish.yaml
@@ -0,0 +1,19 @@ +# A CI configuration to auto-publish pub packages. + +name: Publish + +on: + pull_request: + branches: [ main ] + push: + tags: [ '[A-z]+-v[0-9]+.[0-9]+.[0-9]+' ] + +jobs: + publish: + if: ${{ github.repository_owner == 'dart-lang' }} + uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main + permissions: + id-token: write # Required for authentication using OIDC + pull-requests: write # Required for writing the pull request note + with: + write-comments: false
diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml new file mode 100644 index 0000000..9933aad --- /dev/null +++ b/.github/workflows/pull_request_label.yml
@@ -0,0 +1,22 @@ +# This workflow applies labels to pull requests based on the paths that are +# modified in the pull request. +# +# Edit `.github/labeler.yml` to configure labels. For more information, see +# https://github.com/actions/labeler. + +name: Pull Request Labeler +permissions: read-all + +on: + pull_request_target + +jobs: + label: + permissions: + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true
diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..846e4a1 --- /dev/null +++ b/AUTHORS
@@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the Dart project. Names should be added to the list like so: +# +# Name/Organization <email address> + +Google LLC
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3c28563 --- /dev/null +++ b/CONTRIBUTING.md
@@ -0,0 +1,51 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement (CLA). You (or your employer) retain the copyright to your +contribution; this simply gives us permission to use and redistribute your +contributions as part of the project. Head over to +<https://cla.developers.google.com/> to see your current agreements on file or +to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code Reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Coding style + +The Dart source code in this repo follows the: + + * [Dart style guide](https://dart.dev/guides/language/effective-dart/style) + +You should familiarize yourself with those guidelines. + +## File headers + +All files in the Dart project must start with the following header; if you add a +new file please also add this. The year should be a single number stating the +year the file was created (don't use a range like "2011-2012"). Additionally, if +you edit an existing file, you shouldn't update the year. + + // 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. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). + +We pledge to maintain an open and welcoming environment. For details, see our +[code of conduct](https://dart.dev/code-of-conduct).
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4fd5739 --- /dev/null +++ b/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2023, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed09c1d --- /dev/null +++ b/README.md
@@ -0,0 +1,22 @@ +[](https://coveralls.io/github/dart-lang/native?branch=main) + +## Overview + +This repository is home to Dart packages related to FFI and native assets +building and bundling. + +## Packages + +| Package | Description | Version | +| -------------------------------------------- | ------------------------------------------------------------------------------------------- | ------- | +| [native_assets_builder](pkgs/native_assets_builder/) | A library that contains the logic for building native assets. This should not be used by users, and is used as shared implementation between dartdev and flutter_tools. | [](https://pub.dev/packages/native_assets_builder) | +| [native_assets_cli](pkgs/native_assets_cli/) | A library that contains the argument and file formats for implementing a native assets CLI. | [](https://pub.dev/packages/native_assets_cli) | +| [native_toolchain_c](pkgs/native_toolchain_c/) | A library to invoke the native C compiler installed on the host machine. | [](https://pub.dev/packages/native_toolchain_c) | + +## Publishing automation + +For information about our publishing automation and release process, see +https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. + +For additional information about contributing, see our +[contributing](CONTRIBUTING.md) page.
diff --git a/pkgs/native_assets_builder/.gitignore b/pkgs/native_assets_builder/.gitignore new file mode 100644 index 0000000..9fa208d --- /dev/null +++ b/pkgs/native_assets_builder/.gitignore
@@ -0,0 +1,11 @@ +# Please keep consistent with .pubignore. + +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +coverage/
diff --git a/pkgs/native_assets_builder/.pubignore b/pkgs/native_assets_builder/.pubignore new file mode 100644 index 0000000..1d80e79 --- /dev/null +++ b/pkgs/native_assets_builder/.pubignore
@@ -0,0 +1,14 @@ +# Please keep consistent with .gitignore. + +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +coverage/ + +# Woraround https://github.com/dart-lang/pub/issues/3982 +test/
diff --git a/pkgs/native_assets_builder/CHANGELOG.md b/pkgs/native_assets_builder/CHANGELOG.md new file mode 100644 index 0000000..5700d5d --- /dev/null +++ b/pkgs/native_assets_builder/CHANGELOG.md
@@ -0,0 +1,38 @@ +## 0.3.0 + +- Bump `package:native_assets_cli` to 0.3.0 + ([#142](https://github.com/dart-lang/native/issues/142)). + +## 0.2.3 + +- Quicker build planning for 0 or 1 packages with native assets + ([#128](https://github.com/dart-lang/native/issues/128)). + +## 0.2.2 + +- Take a `PackageLayout` argument for `build` and `dryRun` + ([flutter#134427](https://github.com/flutter/flutter/issues/134427)). + +## 0.2.1 + +- Provide a `PackageLayout` constructor for already parsed `PackageConfig` + ([flutter#134427](https://github.com/flutter/flutter/issues/134427)). + +## 0.2.0 + +- **Breaking change** `NativeAssetsBuildRunner`s methods now return an object + ([#105](https://github.com/dart-lang/native/issues/105)). +- **Breaking change** `NativeAssetsBuildRunner`s methods now return value now + contain a success bool instead of throwing + ([#106](https://github.com/dart-lang/native/issues/106)). Error messages are + streamed to the logger. +- Use an `out/` sub directory for building native assets + ([#98](https://github.com/dart-lang/native/issues/98)). +- Check asset ids on having having a package uri with the owning package + ([#96](https://github.com/dart-lang/native/issues/96)). +- `NativeAssetsBuildRunner` now supports multiple calls + ([#102](https://github.com/dart-lang/native/issues/102)). + +## 0.1.0 + +- Initial version.
diff --git a/pkgs/native_assets_builder/LICENSE b/pkgs/native_assets_builder/LICENSE new file mode 100644 index 0000000..4fd5739 --- /dev/null +++ b/pkgs/native_assets_builder/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2023, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/native_assets_builder/README.md b/pkgs/native_assets_builder/README.md new file mode 100644 index 0000000..dfdc0e8 --- /dev/null +++ b/pkgs/native_assets_builder/README.md
@@ -0,0 +1,19 @@ +[](https://github.com/dart-lang/native/actions/workflows/native.yaml) +[](https://coveralls.io/github/dart-lang/native?branch=main) +[](https://pub.dev/packages/native_assets_builder) +[](https://pub.dev/packages/native_assets_builder/publisher) + +This package contains the shared logic for invoking [native assets CLI]s. + +## Audience + +This package is not intended for users, rather it is shared logic for Dart +launchers to invoke the native assets CLIs of packages. +Known Dart launchers using this shared logic: [dartdev] and [flutter_tools]. + +For more information on how to use native assets as a user see +package [native assets CLI]. + +[native assets CLI]: https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli +[dartdev]: https://github.com/dart-lang/sdk/tree/main/pkg/dartdev +[flutter_tools]: https://github.com/flutter/flutter/tree/master/packages/flutter_tools
diff --git a/pkgs/native_assets_builder/analysis_options.yaml b/pkgs/native_assets_builder/analysis_options.yaml new file mode 100644 index 0000000..89ab6f0 --- /dev/null +++ b/pkgs/native_assets_builder/analysis_options.yaml
@@ -0,0 +1,15 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-raw-types: true + exclude: + # TODO(https://github.com/dart-lang/ecosystem/issues/150): Remove this. + - test/data/ + +linter: + rules: + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_in_for_each + - prefer_final_locals
diff --git a/pkgs/native_assets_builder/dart_test.yaml b/pkgs/native_assets_builder/dart_test.yaml new file mode 100644 index 0000000..c1c8a57 --- /dev/null +++ b/pkgs/native_assets_builder/dart_test.yaml
@@ -0,0 +1,2 @@ +paths: + - test/build_runner/
diff --git a/pkgs/native_assets_builder/lib/native_assets_builder.dart b/pkgs/native_assets_builder/lib/native_assets_builder.dart new file mode 100644 index 0000000..3ff29ab --- /dev/null +++ b/pkgs/native_assets_builder/lib/native_assets_builder.dart
@@ -0,0 +1,6 @@ +// 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. + +export 'package:native_assets_builder/src/build_runner/build_runner.dart'; +export 'package:native_assets_builder/src/package_layout/package_layout.dart';
diff --git a/pkgs/native_assets_builder/lib/src/build_runner/build_planner.dart b/pkgs/native_assets_builder/lib/src/build_runner/build_planner.dart new file mode 100644 index 0000000..24c533a --- /dev/null +++ b/pkgs/native_assets_builder/lib/src/build_runner/build_planner.dart
@@ -0,0 +1,108 @@ +// 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. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:graphs/graphs.dart' as graphs; +import 'package:logging/logging.dart'; +import 'package:package_config/package_config.dart'; + +class NativeAssetsBuildPlanner { + final PackageGraph packageGraph; + final List<Package> packagesWithNativeAssets; + final Uri dartExecutable; + final Logger logger; + + NativeAssetsBuildPlanner({ + required this.packageGraph, + required this.packagesWithNativeAssets, + required this.dartExecutable, + required this.logger, + }); + + static Future<NativeAssetsBuildPlanner> fromRootPackageRoot({ + required Uri rootPackageRoot, + required List<Package> packagesWithNativeAssets, + required Uri dartExecutable, + required Logger logger, + }) async { + final result = await Process.run( + dartExecutable.toFilePath(), + [ + 'pub', + 'deps', + '--json', + ], + workingDirectory: rootPackageRoot.toFilePath(), + ); + final packageGraph = + PackageGraph.fromPubDepsJsonString(result.stdout as String); + return NativeAssetsBuildPlanner( + packageGraph: packageGraph, + packagesWithNativeAssets: packagesWithNativeAssets, + dartExecutable: dartExecutable, + logger: logger, + ); + } + + (List<Package> packages, bool success) plan() { + final packageMap = { + for (final package in packagesWithNativeAssets) package.name: package + }; + final packagesToBuild = packageMap.keys.toSet(); + final stronglyConnectedComponents = packageGraph.computeStrongComponents(); + final result = <Package>[]; + var success = true; + for (final stronglyConnectedComponent in stronglyConnectedComponents) { + final stronglyConnectedComponentWithNativeAssets = [ + for (final packageName in stronglyConnectedComponent) + if (packagesToBuild.contains(packageName)) packageName + ]; + if (stronglyConnectedComponentWithNativeAssets.length > 1) { + logger.severe( + 'Cyclic dependency for native asset builds in the following ' + 'packages: $stronglyConnectedComponentWithNativeAssets.', + ); + success = false; + } else if (stronglyConnectedComponentWithNativeAssets.length == 1) { + result.add( + packageMap[stronglyConnectedComponentWithNativeAssets.single]!); + } + } + return (result, success); + } +} + +class PackageGraph { + final Map<String, List<String>> map; + + PackageGraph(this.map); + + /// Construct a graph from the JSON produced by `dart pub deps --json`. + factory PackageGraph.fromPubDepsJsonString(String json) => + PackageGraph.fromPubDepsJson(jsonDecode(json) as Map<dynamic, dynamic>); + + /// Construct a graph from the JSON produced by `dart pub deps --json`. + factory PackageGraph.fromPubDepsJson(Map<dynamic, dynamic> map) { + final result = <String, List<String>>{}; + final packages = map['packages'] as List<dynamic>; + for (final package in packages) { + final package_ = package as Map<dynamic, dynamic>; + final name = package_['name'] as String; + final dependencies = (package_['dependencies'] as List<dynamic>) + .whereType<String>() + .toList(); + result[name] = dependencies; + } + return PackageGraph(result); + } + + Iterable<String> neighborsOf(String vertex) => map[vertex] ?? []; + + Iterable<String> get vertices => map.keys; + + List<List<String>> computeStrongComponents() => + graphs.stronglyConnectedComponents(vertices, neighborsOf); +}
diff --git a/pkgs/native_assets_builder/lib/src/build_runner/build_runner.dart b/pkgs/native_assets_builder/lib/src/build_runner/build_runner.dart new file mode 100644 index 0000000..579855c --- /dev/null +++ b/pkgs/native_assets_builder/lib/src/build_runner/build_runner.dart
@@ -0,0 +1,478 @@ +// 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:package_config/package_config.dart'; + +import '../package_layout/package_layout.dart'; +import '../utils/run_process.dart'; +import 'build_planner.dart'; + +typedef DependencyMetadata = Map<String, Metadata>; + +/// The programmatic API to be used by Dart launchers to invoke native builds. +/// +/// These methods are invoked by launchers such as dartdev (for `dart run`) +/// and flutter_tools (for `flutter run` and `flutter build`). +class NativeAssetsBuildRunner { + final Logger logger; + final Uri dartExecutable; + + NativeAssetsBuildRunner({ + required this.logger, + required this.dartExecutable, + }); + + /// [workingDirectory] is expected to contain `.dart_tool`. + /// + /// This method is invoked by launchers such as dartdev (for `dart run`) and + /// flutter_tools (for `flutter run` and `flutter build`). + /// + /// Completes the future with an error if the build fails. + Future<BuildResult> build({ + required LinkModePreference linkModePreference, + required Target target, + required Uri workingDirectory, + required BuildMode buildMode, + CCompilerConfig? cCompilerConfig, + IOSSdk? targetIOSSdk, + int? targetAndroidNdkApi, + required bool includeParentEnvironment, + PackageLayout? packageLayout, + }) async { + packageLayout ??= await PackageLayout.fromRootPackageRoot(workingDirectory); + final packagesWithNativeAssets = + await packageLayout.packagesWithNativeAssets; + final List<Package> buildPlan; + final PackageGraph packageGraph; + if (packagesWithNativeAssets.length <= 1) { + buildPlan = packagesWithNativeAssets; + packageGraph = PackageGraph({ + for (final p in packagesWithNativeAssets) p.name: [], + }); + } else { + final planner = await NativeAssetsBuildPlanner.fromRootPackageRoot( + rootPackageRoot: packageLayout.rootPackageRoot, + packagesWithNativeAssets: packagesWithNativeAssets, + dartExecutable: Uri.file(Platform.resolvedExecutable), + logger: logger, + ); + final (plan, planSuccess) = planner.plan(); + if (!planSuccess) { + return _BuildResultImpl( + assets: [], + dependencies: [], + success: false, + ); + } + buildPlan = plan; + packageGraph = planner.packageGraph; + } + final assets = <Asset>[]; + final dependencies = <Uri>[]; + final metadata = <String, Metadata>{}; + var success = true; + for (final package in buildPlan) { + final dependencyMetadata = _metadataForPackage( + packageGraph: packageGraph, + packageName: package.name, + targetMetadata: metadata, + ); + final config = await _cliConfig( + packageName: package.name, + packageRoot: packageLayout.packageRoot(package.name), + target: target, + buildMode: buildMode, + linkMode: linkModePreference, + buildParentDir: packageLayout.dartToolNativeAssetsBuilder, + dependencyMetadata: dependencyMetadata, + cCompilerConfig: cCompilerConfig, + targetIOSSdk: targetIOSSdk, + targetAndroidNdkApi: targetAndroidNdkApi, + ); + final ( + packageAssets, + packageDependencies, + packageMetadata, + packageSuccess, + ) = await _buildPackageCached( + config, + packageLayout.packageConfigUri, + workingDirectory, + includeParentEnvironment, + ); + assets.addAll(packageAssets); + dependencies.addAll(packageDependencies); + success &= packageSuccess; + if (packageMetadata != null) { + metadata[config.packageName] = packageMetadata; + } + } + return _BuildResultImpl( + assets: assets, + dependencies: dependencies..sort(_uriCompare), + success: success, + ); + } + + /// [workingDirectory] is expected to contain `.dart_tool`. + /// + /// This method is invoked by launchers such as dartdev (for `dart run`) and + /// flutter_tools (for `flutter run` and `flutter build`). + /// + /// Completes the future with an error if the build fails. + Future<DryRunResult> dryRun({ + required LinkModePreference linkModePreference, + required OS targetOs, + required Uri workingDirectory, + required bool includeParentEnvironment, + PackageLayout? packageLayout, + }) async { + packageLayout ??= await PackageLayout.fromRootPackageRoot(workingDirectory); + final packagesWithNativeAssets = + await packageLayout.packagesWithNativeAssets; + final List<Package> buildPlan; + if (packagesWithNativeAssets.length <= 1) { + buildPlan = packagesWithNativeAssets; + } else { + final planner = await NativeAssetsBuildPlanner.fromRootPackageRoot( + rootPackageRoot: packageLayout.rootPackageRoot, + packagesWithNativeAssets: packagesWithNativeAssets, + dartExecutable: Uri.file(Platform.resolvedExecutable), + logger: logger, + ); + final (plan, planSuccess) = planner.plan(); + if (!planSuccess) { + return _DryRunResultImpl( + assets: [], + success: false, + ); + } + buildPlan = plan; + } + final assets = <Asset>[]; + var success = true; + for (final package in buildPlan) { + final config = await _cliConfigDryRun( + packageName: package.name, + packageRoot: packageLayout.packageRoot(package.name), + targetOs: targetOs, + linkMode: linkModePreference, + buildParentDir: packageLayout.dartToolNativeAssetsBuilder, + ); + final (packageAssets, _, _, packageSuccess) = await _buildPackage( + config, + packageLayout.packageConfigUri, + workingDirectory, + includeParentEnvironment, + dryRun: true, + ); + assets.addAll(packageAssets); + success &= packageSuccess; + } + return _DryRunResultImpl( + assets: assets, + success: success, + ); + } + + Future<_PackageBuildRecord> _buildPackageCached( + BuildConfig config, + Uri packageConfigUri, + Uri workingDirectory, + bool includeParentEnvironment, + ) async { + final packageName = config.packageName; + final outDir = config.outDir; + if (!await Directory.fromUri(outDir).exists()) { + await Directory.fromUri(outDir).create(recursive: true); + } + + final buildOutput = await BuildOutput.readFromFile(outDir: outDir); + final lastBuilt = buildOutput?.timestamp.roundDownToSeconds() ?? + DateTime.fromMillisecondsSinceEpoch(0); + final dependencies = buildOutput?.dependencies; + final lastChange = await dependencies?.lastModified() ?? DateTime.now(); + + if (lastBuilt.isAfter(lastChange)) { + logger.info('Skipping build for $packageName in $outDir. ' + 'Last build on $lastBuilt, last input change on $lastChange.'); + // All build flags go into [outDir]. Therefore we do not have to check + // here whether the config is equal. + final assets = buildOutput!.assets; + final dependencies = buildOutput.dependencies.dependencies; + final metadata = buildOutput.metadata; + return (assets, dependencies, metadata, true); + } + + return await _buildPackage( + config, + packageConfigUri, + workingDirectory, + includeParentEnvironment, + dryRun: false, + ); + } + + Future<_PackageBuildRecord> _buildPackage( + BuildConfig config, + Uri packageConfigUri, + Uri workingDirectory, + bool includeParentEnvironment, { + required bool dryRun, + }) async { + final outDir = config.outDir; + final configFile = outDir.resolve('../config.yaml'); + final buildDotDart = config.packageRoot.resolve('build.dart'); + final configFileContents = config.toYamlString(); + logger.info('config.yaml contents: $configFileContents'); + await File.fromUri(configFile).writeAsString(configFileContents); + final buildOutputFile = File.fromUri(outDir.resolve(BuildOutput.fileName)); + if (await buildOutputFile.exists()) { + // Ensure we'll never read outdated build results. + await buildOutputFile.delete(); + } + final arguments = [ + '--packages=${packageConfigUri.toFilePath()}', + buildDotDart.toFilePath(), + '--config=${configFile.toFilePath()}', + ]; + final result = await runProcess( + workingDirectory: workingDirectory, + executable: dartExecutable, + arguments: arguments, + logger: logger, + includeParentEnvironment: includeParentEnvironment, + ); + var success = true; + if (result.exitCode != 0) { + final printWorkingDir = workingDirectory != Directory.current.uri; + final commandString = [ + if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};', + dartExecutable.toFilePath(), + ...arguments.map((a) => a.contains(' ') ? "'$a'" : a), + if (printWorkingDir) ')', + ].join(' '); + logger.severe( + ''' +Building native assets for package:${config.packageName} failed. +build.dart returned with exit code: ${result.exitCode}. +To reproduce run: +$commandString +stderr: +${result.stderr} +stdout: +${result.stdout} + ''', + ); + success = false; + } + + try { + final buildOutput = await BuildOutput.readFromFile(outDir: outDir); + final assets = buildOutput?.assets ?? []; + success &= validateAssetsPackage(assets, config.packageName); + final dependencies = buildOutput?.dependencies.dependencies ?? []; + final metadata = dryRun ? null : buildOutput?.metadata; + return (assets, dependencies, metadata, success); + } on FormatException catch (e) { + logger.severe(''' +Building native assets for package:${config.packageName} failed. +build_output.yaml contained a format error. +${e.message} + '''); + success = false; + return (<Asset>[], <Uri>[], const Metadata({}), false); + // TODO(https://github.com/dart-lang/native/issues/109): Stop throwing + // type errors in native_assets_cli, release a new version of that package + // and then remove this. + // ignore: avoid_catching_errors + } on TypeError { + logger.severe(''' +Building native assets for package:${config.packageName} failed. +build_output.yaml contained a format error. + '''); + success = false; + return (<Asset>[], <Uri>[], const Metadata({}), false); + } finally { + if (!success) { + final buildOutputFile = + File.fromUri(outDir.resolve(BuildOutput.fileName)); + if (await buildOutputFile.exists()) { + await buildOutputFile.delete(); + } + } + } + } + + static Future<BuildConfig> _cliConfig({ + required String packageName, + required Uri packageRoot, + required Target target, + IOSSdk? targetIOSSdk, + int? targetAndroidNdkApi, + required BuildMode buildMode, + required LinkModePreference linkMode, + required Uri buildParentDir, + CCompilerConfig? cCompilerConfig, + DependencyMetadata? dependencyMetadata, + }) async { + final buildDirName = BuildConfig.checksum( + packageName: packageName, + packageRoot: packageRoot, + targetOs: target.os, + targetArchitecture: target.architecture, + buildMode: buildMode, + linkModePreference: linkMode, + targetIOSSdk: targetIOSSdk, + cCompiler: cCompilerConfig, + dependencyMetadata: dependencyMetadata, + targetAndroidNdkApi: targetAndroidNdkApi, + ); + final outDirUri = buildParentDir.resolve('$buildDirName/out/'); + final outDir = Directory.fromUri(outDirUri); + if (!await outDir.exists()) { + // TODO(https://dartbug.com/50565): Purge old or unused folders. + await outDir.create(recursive: true); + } + return BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: packageRoot, + targetOs: target.os, + targetArchitecture: target.architecture, + buildMode: buildMode, + linkModePreference: linkMode, + targetIOSSdk: targetIOSSdk, + cCompiler: cCompilerConfig, + dependencyMetadata: dependencyMetadata, + targetAndroidNdkApi: targetAndroidNdkApi, + ); + } + + static Future<BuildConfig> _cliConfigDryRun({ + required String packageName, + required Uri packageRoot, + required OS targetOs, + required LinkModePreference linkMode, + required Uri buildParentDir, + }) async { + final buildDirName = 'dry_run_${targetOs}_$linkMode'; + final outDirUri = buildParentDir.resolve('$buildDirName/out/'); + final outDir = Directory.fromUri(outDirUri); + if (!await outDir.exists()) { + await outDir.create(recursive: true); + } + return BuildConfig.dryRun( + outDir: outDirUri, + packageName: packageName, + packageRoot: packageRoot, + targetOs: targetOs, + linkModePreference: linkMode, + ); + } + + DependencyMetadata? _metadataForPackage({ + required PackageGraph packageGraph, + required String packageName, + DependencyMetadata? targetMetadata, + }) { + if (targetMetadata == null) { + return null; + } + final dependencies = packageGraph.neighborsOf(packageName).toSet(); + return { + for (final entry in targetMetadata.entries) + if (dependencies.contains(entry.key)) entry.key: entry.value, + }; + } + + bool validateAssetsPackage(List<Asset> assets, String packageName) { + final invalidAssetIds = assets + .map((a) => a.id) + .where((n) => !n.startsWith('package:$packageName/')) + .toSet() + .toList() + ..sort(); + final success = invalidAssetIds.isEmpty; + if (!success) { + logger.severe( + '`package:$packageName` declares the following assets which do not ' + 'start with `package:$packageName/`: ${invalidAssetIds.join(', ')}.', + ); + } + return success; + } +} + +typedef _PackageBuildRecord = ( + List<Asset>, + List<Uri> dependencies, + Metadata?, + bool success, +); + +/// The result from a [NativeAssetsBuildRunner.dryRun]. +abstract interface class DryRunResult { + /// The native assets for all [Target]s for the build or dry run. + List<Asset> get assets; + + /// Whether all builds completed without errors. + /// + /// All error messages are streamed to [NativeAssetsBuildRunner.logger]. + bool get success; +} + +final class _DryRunResultImpl implements DryRunResult { + @override + final List<Asset> assets; + + @override + final bool success; + + _DryRunResultImpl({ + required this.assets, + required this.success, + }); +} + +/// The result from a [NativeAssetsBuildRunner.build]. +abstract class BuildResult implements DryRunResult { + /// All the files used for building the native assets of all packages. + /// + /// This aggregated list can be used to determine whether the + /// [NativeAssetsBuildRunner] needs to be invoked again. The + /// [NativeAssetsBuildRunner] determines per package with native assets + /// if it needs to run the build again. + List<Uri> get dependencies; +} + +final class _BuildResultImpl implements BuildResult { + @override + final List<Asset> assets; + + @override + final List<Uri> dependencies; + + @override + final bool success; + + _BuildResultImpl({ + required this.assets, + required this.dependencies, + required this.success, + }); +} + +extension on DateTime { + DateTime roundDownToSeconds() => + DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch - + millisecondsSinceEpoch % const Duration(seconds: 1).inMilliseconds); +} + +int _uriCompare(Uri u1, Uri u2) => u1.toString().compareTo(u2.toString());
diff --git a/pkgs/native_assets_builder/lib/src/package_layout/package_layout.dart b/pkgs/native_assets_builder/lib/src/package_layout/package_layout.dart new file mode 100644 index 0000000..00b97d2 --- /dev/null +++ b/pkgs/native_assets_builder/lib/src/package_layout/package_layout.dart
@@ -0,0 +1,104 @@ +// 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. + +import 'dart:io'; + +import 'package:package_config/package_config.dart'; + +/// Directory layout for dealing with native assets. +/// +/// Build scripts for native assets will be run from the context of another +/// root package. +/// +/// The directory layout follows pub's convention for caching: +/// https://dart.dev/tools/pub/package-layout#project-specific-caching-for-tools +class PackageLayout { + /// The root folder of the current dart invocation root package. + /// + /// `$rootPackageRoot`. + final Uri rootPackageRoot; + + /// Package config containing the information of where to foot the root [Uri]s + /// of other packages. + /// + /// Can be `null` to enable quick construction of a + /// [PackageLayout]. + final PackageConfig packageConfig; + + final Uri packageConfigUri; + + PackageLayout._( + this.rootPackageRoot, this.packageConfig, this.packageConfigUri); + + factory PackageLayout.fromPackageConfig( + PackageConfig packageConfig, + Uri packageConfigUri, + ) { + assert(File.fromUri(packageConfigUri).existsSync()); + packageConfigUri = packageConfigUri.normalizePath(); + final rootPackageRoot = packageConfigUri.resolve('../'); + return PackageLayout._(rootPackageRoot, packageConfig, packageConfigUri); + } + + static Future<PackageLayout> fromRootPackageRoot(Uri rootPackageRoot) async { + rootPackageRoot = rootPackageRoot.normalizePath(); + final packageConfigUri = + rootPackageRoot.resolve('.dart_tool/package_config.json'); + assert(await File.fromUri(packageConfigUri).exists()); + final packageConfig = await loadPackageConfigUri(packageConfigUri); + return PackageLayout._(rootPackageRoot, packageConfig, packageConfigUri); + } + + /// The .dart_tool directory is used to store built artifacts and caches. + /// + /// `$rootPackageRoot/.dart_tool/`. + /// + /// Each package should only modify the subfolder of `.dart_tool/` with its + /// own name. + /// https://dart.dev/tools/pub/package-layout#project-specific-caching-for-tools + late final Uri dartTool = rootPackageRoot.resolve('.dart_tool/'); + + /// The directory where `package:native_assets_builder` stores all persistent + /// information. + /// + /// This folder is owned by `package:native_assets_builder`, no other package + /// should read or modify it. + /// https://dart.dev/tools/pub/package-layout#project-specific-caching-for-tools + /// + /// `$rootPackageRoot/.dart_tool/native_assets_builder/`. + late final Uri dartToolNativeAssetsBuilder = + dartTool.resolve('native_assets_builder/'); + + /// The root of `package:$packageName`. + /// + /// `$packageName/`. + /// + /// This folder is owned by pub, and should _never_ be written to. + Uri packageRoot(String packageName) { + final package = packageConfig[packageName]; + if (package == null) { + throw StateError('Package $packageName not found in packageConfig.'); + } + return package.root; + } + + /// All packages in [packageConfig] with native assets. + /// + /// Whether a package has native assets is defined by whether it contains + /// a `build.dart`. + /// + /// `package:native` itself is excluded. + late final Future<List<Package>> packagesWithNativeAssets = () async { + final result = <Package>[]; + for (final package in packageConfig.packages) { + final packageRoot = package.root; + if (packageRoot.scheme == 'file') { + if (await File.fromUri(packageRoot.resolve('build.dart')).exists()) { + result.add(package); + } + } + } + return result; + }(); +}
diff --git a/pkgs/native_assets_builder/lib/src/utils/run_process.dart b/pkgs/native_assets_builder/lib/src/utils/run_process.dart new file mode 100644 index 0000000..cb989c7 --- /dev/null +++ b/pkgs/native_assets_builder/lib/src/utils/run_process.dart
@@ -0,0 +1,132 @@ +// 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +/// Runs a [Process]. +/// +/// If [logger] is provided, stream stdout and stderr to it. +/// +/// If [captureOutput], captures stdout and stderr. +// TODO(dacoharkes): Share between package:native_toolchain_c and here. +Future<RunProcessResult> runProcess({ + required Uri executable, + List<String> arguments = const [], + Uri? workingDirectory, + Map<String, String>? environment, + bool includeParentEnvironment = true, + required Logger? logger, + bool captureOutput = true, + int expectedExitCode = 0, + bool throwOnUnexpectedExitCode = false, +}) async { + if (Platform.isWindows && !includeParentEnvironment) { + const winEnvKeys = [ + 'SYSTEMROOT', + 'TEMP', + 'TMP', + ]; + environment = { + for (final winEnvKey in winEnvKeys) + winEnvKey: Platform.environment[winEnvKey]!, + ...?environment, + }; + } + + final printWorkingDir = + workingDirectory != null && workingDirectory != Directory.current.uri; + final commandString = [ + if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};', + ...?environment?.entries.map((entry) => '${entry.key}=${entry.value}'), + executable.toFilePath(), + ...arguments.map((a) => a.contains(' ') ? "'$a'" : a), + if (printWorkingDir) ')', + ].join(' '); + logger?.info('Running `$commandString`.'); + + final stdoutBuffer = StringBuffer(); + final stderrBuffer = StringBuffer(); + final process = await Process.start( + executable.toFilePath(), + arguments, + workingDirectory: workingDirectory?.toFilePath(), + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: Platform.isWindows && + (!includeParentEnvironment || workingDirectory != null), + ); + + final stdoutSub = process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(captureOutput + ? (s) { + logger?.fine(s); + stdoutBuffer.writeln(s); + } + : logger?.fine); + final stderrSub = process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(captureOutput + ? (s) { + logger?.severe(s); + stderrBuffer.writeln(s); + } + : logger?.severe); + + final (exitCode, _, _) = await ( + process.exitCode, + stdoutSub.asFuture<void>(), + stderrSub.asFuture<void>() + ).wait; + final result = RunProcessResult( + pid: process.pid, + command: commandString, + exitCode: exitCode, + stdout: stdoutBuffer.toString(), + stderr: stderrBuffer.toString(), + ); + if (throwOnUnexpectedExitCode && expectedExitCode != exitCode) { + throw ProcessException( + executable.toFilePath(), + arguments, + "Full command string: '$commandString'.\n" + "Exit code: '$exitCode'.\n" + 'For the output of the process check the logger output.', + ); + } + return result; +} + +/// Drop in replacement of [ProcessResult]. +class RunProcessResult { + final int pid; + + final String command; + + final int exitCode; + + final String stderr; + + final String stdout; + + RunProcessResult({ + required this.pid, + required this.command, + required this.exitCode, + required this.stderr, + required this.stdout, + }); + + @override + String toString() => '''command: $command +exitCode: $exitCode +stdout: $stdout +stderr: $stderr'''; +}
diff --git a/pkgs/native_assets_builder/pubspec.yaml b/pkgs/native_assets_builder/pubspec.yaml new file mode 100644 index 0000000..af8c48e --- /dev/null +++ b/pkgs/native_assets_builder/pubspec.yaml
@@ -0,0 +1,20 @@ +name: native_assets_builder +description: >- + This package is the backend that invokes top-level `build.dart` scripts. +version: 0.3.0 +repository: https://github.com/dart-lang/native/tree/main/pkgs/native_assets_builder + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + graphs: ^2.3.1 + logging: ^1.2.0 + native_assets_cli: ^0.3.0 + package_config: ^2.1.0 + +dev_dependencies: + dart_flutter_team_lints: ^2.1.1 + file_testing: ^3.0.0 + test: ^1.24.3 + yaml: ^3.1.2
diff --git a/pkgs/native_assets_builder/test/build_runner/build_dependencies_test.dart b/pkgs/native_assets_builder/test/build_runner/build_dependencies_test.dart new file mode 100644 index 0000000..4e5de91 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_dependencies_test.dart
@@ -0,0 +1,55 @@ +// 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. + +import 'dart:io'; + +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('dart_app build dependencies', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('dart_app/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + // Trigger a build, should invoke build for libraries with native assets. + { + final logMessages = <String>[]; + final result = await build(packageUri, logger, dartExecutable, + capturedLogs: logMessages); + expect( + logMessages.join('\n'), + stringContainsInOrder( + [ + 'native_add${Platform.pathSeparator}build.dart', + 'native_subtract${Platform.pathSeparator}build.dart' + ], + ), + ); + expect(result.assets.length, 2); + expect( + result.dependencies, + [ + tempUri.resolve('native_add/').resolve('build.dart'), + tempUri.resolve('native_add/').resolve('src/native_add.c'), + tempUri.resolve('native_subtract/').resolve('build.dart'), + tempUri + .resolve('native_subtract/') + .resolve('src/native_subtract.c'), + ], + ); + } + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_planner_test.dart b/pkgs/native_assets_builder/test/build_runner/build_planner_test.dart new file mode 100644 index 0000000..48daae3 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_planner_test.dart
@@ -0,0 +1,77 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_builder/native_assets_builder.dart'; +import 'package:native_assets_builder/src/build_runner/build_planner.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +void main() async { + test('build dependency graph from pub', () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final nativeAddUri = tempUri.resolve('native_add/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet(workingDirectory: nativeAddUri, logger: logger); + + final result = await runProcess( + executable: Uri.file(Platform.resolvedExecutable), + arguments: [ + 'pub', + 'deps', + '--json', + ], + workingDirectory: nativeAddUri, + logger: logger, + ); + expect(result.exitCode, 0); + + final graph = PackageGraph.fromPubDepsJsonString(result.stdout); + + final packageLayout = + await PackageLayout.fromRootPackageRoot(nativeAddUri); + final packagesWithNativeAssets = + await packageLayout.packagesWithNativeAssets; + + final planner = NativeAssetsBuildPlanner( + packageGraph: graph, + packagesWithNativeAssets: packagesWithNativeAssets, + dartExecutable: Uri.file(Platform.resolvedExecutable), + logger: logger, + ); + final (buildPlan, _) = planner.plan(); + expect(buildPlan.length, 1); + expect(buildPlan.single.name, 'native_add'); + }); + }); + test('build dependency graph fromPackageRoot', () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final nativeAddUri = tempUri.resolve('native_add/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet(workingDirectory: nativeAddUri, logger: logger); + + final packageLayout = + await PackageLayout.fromRootPackageRoot(nativeAddUri); + final packagesWithNativeAssets = + await packageLayout.packagesWithNativeAssets; + final nativeAssetsBuildPlanner = + await NativeAssetsBuildPlanner.fromRootPackageRoot( + rootPackageRoot: nativeAddUri, + packagesWithNativeAssets: packagesWithNativeAssets, + dartExecutable: Uri.file(Platform.resolvedExecutable), + logger: logger, + ); + final (buildPlan, _) = nativeAssetsBuildPlanner.plan(); + expect(buildPlan.length, 1); + expect(buildPlan.single.name, 'native_add'); + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_asset_id_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_asset_id_test.dart new file mode 100644 index 0000000..19ad319 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_asset_id_test.dart
@@ -0,0 +1,67 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('wrong asset id', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('wrong_namespace_asset/'); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + { + final logMessages = <String>[]; + final result = await build( + packageUri, + createCapturingLogger(logMessages, level: Level.SEVERE), + dartExecutable, + ); + final fullLog = logMessages.join('\n'); + expect(result.success, false); + expect( + fullLog, + contains( + '`package:wrong_namespace_asset` declares the following assets ' + 'which do not start with `package:wrong_namespace_asset/`:', + ), + ); + } + }); + }); + + test('right asset id but other directory', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + final packageUri = tempUri.resolve('different_root_dir/'); + await copyTestProjects( + sourceUri: testDataUri.resolve('native_add/'), + targetUri: packageUri, + ); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + { + final result = await build( + packageUri, + logger, + dartExecutable, + ); + expect(result.success, true); + } + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_build_output_format_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_build_output_format_test.dart new file mode 100644 index 0000000..21f15f9 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_build_output_format_test.dart
@@ -0,0 +1,55 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + for (final package in [ + 'wrong_build_output', + 'wrong_build_output_2', + 'wrong_build_output_3', + ]) { + test('wrong build output $package', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('$package/'); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + // Run twice, failures should not be cached and return the same errors. + for (final _ in [1, 2]) { + final logMessages = <String>[]; + final result = await build( + packageUri, + createCapturingLogger(logMessages, level: Level.SEVERE), + dartExecutable, + ); + final fullLog = logMessages.join('\n'); + expect(result.success, false); + if (package == 'wrong_build_output_3') { + // Should re-execute the process on second run. + expect( + fullLog, + contains('build.dart returned with exit code: 1.'), + ); + } else { + expect( + fullLog, + contains('build_output.yaml contained a format error.'), + ); + } + } + }); + }); + } +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_caching_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_caching_test.dart new file mode 100644 index 0000000..2749e53 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_caching_test.dart
@@ -0,0 +1,118 @@ +// 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. + +import 'dart:io'; + +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('cached build', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + { + final logMessages = <String>[]; + final result = await build(packageUri, logger, dartExecutable, + capturedLogs: logMessages); + expect( + logMessages.join('\n'), + contains('native_add${Platform.pathSeparator}build.dart'), + ); + expect( + result.dependencies, + [ + packageUri.resolve('build.dart'), + packageUri.resolve('src/native_add.c'), + ], + ); + } + + { + final logMessages = <String>[]; + final result = await build(packageUri, logger, dartExecutable, + capturedLogs: logMessages); + expect( + logMessages.join('\n'), + contains('Skipping build for native_add'), + ); + expect( + logMessages.join('\n'), + isNot(contains('native_add${Platform.pathSeparator}build.dart')), + ); + expect( + result.dependencies, + [ + packageUri.resolve('build.dart'), + packageUri.resolve('src/native_add.c'), + ], + ); + } + }); + }); + + test('modify C file', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + { + final result = await build(packageUri, logger, dartExecutable); + await expectSymbols(asset: result.assets.single, symbols: ['add']); + } + + await copyTestProjects( + sourceUri: testDataUri.resolve('native_add_add_symbol/'), + targetUri: packageUri, + ); + + { + final result = await build(packageUri, logger, dartExecutable); + await expectSymbols( + asset: result.assets.single, + symbols: ['add', 'subtract'], + ); + } + }); + }); + + test('add C file, modify script', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + await runPubGet(workingDirectory: packageUri, logger: logger); + + { + final result = await build(packageUri, logger, dartExecutable); + await expectSymbols(asset: result.assets.single, symbols: ['add']); + } + + await copyTestProjects( + sourceUri: testDataUri.resolve('native_add_add_source/'), + targetUri: packageUri); + + { + final result = await build(packageUri, logger, dartExecutable); + await expectSymbols( + asset: result.assets.single, symbols: ['add', 'multiply']); + } + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_cycle_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_cycle_test.dart new file mode 100644 index 0000000..94a09c7 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_cycle_test.dart
@@ -0,0 +1,60 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('cycle', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('cyclic_package_1/'); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + { + final logMessages = <String>[]; + final result = await dryRun( + packageUri, + createCapturingLogger(logMessages, level: Level.SEVERE), + dartExecutable, + ); + final fullLog = logMessages.join('\n'); + expect(result.success, false); + expect( + fullLog, + contains( + 'Cyclic dependency for native asset builds in the following ' + 'packages: [cyclic_package_1, cyclic_package_2]', + ), + ); + } + + { + final logMessages = <String>[]; + final result = await build( + packageUri, + createCapturingLogger(logMessages, level: Level.SEVERE), + dartExecutable, + ); + final fullLog = logMessages.join('\n'); + expect(result.success, false); + expect( + fullLog, + contains( + 'Cyclic dependency for native asset builds in the following ' + 'packages: [cyclic_package_1, cyclic_package_2]', + ), + ); + } + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_dry_run_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_dry_run_test.dart new file mode 100644 index 0000000..be4f817 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_dry_run_test.dart
@@ -0,0 +1,50 @@ +// 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. + +import 'dart:io'; + +import 'package:file_testing/file_testing.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('dry_run', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + final dryRunAssets = (await dryRun(packageUri, logger, dartExecutable)) + .assets + .where((element) => element.target == Target.current) + .toList(); + final result = await build(packageUri, logger, dartExecutable); + + expect(dryRunAssets.length, result.assets.length); + for (var i = 0; i < dryRunAssets.length; i++) { + final dryRunAsset = dryRunAssets[0]; + final buildAsset = result.assets[0]; + expect(dryRunAsset.linkMode, buildAsset.linkMode); + expect(dryRunAsset.id, buildAsset.id); + expect(dryRunAsset.target, buildAsset.target); + // The target folders are different, so the paths are different. + } + + final dryRunDir = packageUri.resolve( + '.dart_tool/native_assets_builder/dry_run_${Target.current.os}_dynamic/'); + expect(File.fromUri(dryRunDir.resolve('config.yaml')), exists); + expect(File.fromUri(dryRunDir.resolve('out/build_output.yaml')), exists); + // + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_failure_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_failure_test.dart new file mode 100644 index 0000000..c844be2 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_failure_test.dart
@@ -0,0 +1,83 @@ +// 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. + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('break build', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + { + final result = await build(packageUri, logger, dartExecutable); + expect(result.assets.length, 1); + await expectSymbols(asset: result.assets.single, symbols: ['add']); + expect( + result.dependencies, + [ + packageUri.resolve('build.dart'), + packageUri.resolve('src/native_add.c'), + ], + ); + } + + await copyTestProjects( + sourceUri: testDataUri.resolve('native_add_break_build/'), + targetUri: packageUri, + ); + + { + final logMessages = <String>[]; + final result = await build( + packageUri, + createCapturingLogger(logMessages, level: Level.SEVERE), + dartExecutable, + ); + final fullLog = logMessages.join('\n'); + expect(result.success, false); + expect(fullLog, contains('To reproduce run:')); + final reproCommand = fullLog + .split('\n') + .skipWhile((l) => l != 'To reproduce run:') + .skip(1) + .first; + final reproResult = + await Process.run(reproCommand, [], runInShell: true); + expect(reproResult.exitCode, isNot(0)); + } + + await copyTestProjects( + sourceUri: testDataUri.resolve('native_add_fix_build/'), + targetUri: packageUri, + ); + + { + final result = await build(packageUri, logger, dartExecutable); + expect(result.assets.length, 1); + await expectSymbols(asset: result.assets.single, symbols: ['add']); + expect( + result.dependencies, + [ + packageUri.resolve('build.dart'), + packageUri.resolve('src/native_add.c'), + ], + ); + } + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_reusability_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_reusability_test.dart new file mode 100644 index 0000000..f4313cd --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_reusability_test.dart
@@ -0,0 +1,59 @@ +// 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. + +import 'package:native_assets_builder/src/build_runner/build_runner.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('multiple dryRun and build invocations', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('package_reading_metadata/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + final buildRunner = NativeAssetsBuildRunner( + logger: logger, + dartExecutable: dartExecutable, + ); + + await buildRunner.dryRun( + targetOs: Target.current.os, + linkModePreference: LinkModePreference.dynamic, + workingDirectory: packageUri, + includeParentEnvironment: true, + ); + await buildRunner.dryRun( + targetOs: Target.current.os, + linkModePreference: LinkModePreference.dynamic, + workingDirectory: packageUri, + includeParentEnvironment: true, + ); + await buildRunner.build( + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + target: Target.current, + workingDirectory: packageUri, + includeParentEnvironment: true, + ); + await buildRunner.build( + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + target: Target.current, + workingDirectory: packageUri, + includeParentEnvironment: true, + ); + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_run_in_isolation_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_run_in_isolation_test.dart new file mode 100644 index 0000000..ffcbdc0 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_run_in_isolation_test.dart
@@ -0,0 +1,72 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + String unparseKey(String key) => key.replaceAll('.', '__').toUpperCase(); + final arKey = unparseKey(CCompilerConfig.arConfigKeyFull); + final ccKey = unparseKey(CCompilerConfig.ccConfigKeyFull); + final ldKey = unparseKey(CCompilerConfig.ldConfigKeyFull); + final envScriptKey = unparseKey(CCompilerConfig.envScriptConfigKeyFull); + final envScriptArgsKey = + unparseKey(CCompilerConfig.envScriptArgsConfigKeyFull); + + final cc = Platform.environment[ccKey]?.fileUri; + + if (cc == null) { + // We don't set any compiler paths on the GitHub CI. + // We neither set compiler paths on MacOS on the Dart SDK CI + // in pkg/test_runner/lib/src/configuration.dart + // nativeCompilerEnvironmentVariables. + // + // We could potentially run this test if we default to some compilers + // we find on the path before running the test. However, the logic for + // discovering compilers is currently hidden inside + // package:native_toolchain_c. + return; + } + + test('run in isolation', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + await runPubGet(workingDirectory: packageUri, logger: logger); + + printOnFailure( + 'Platform.environment[ccKey]: ${Platform.environment[ccKey]}'); + printOnFailure('cc: $cc'); + + final result = await build( + packageUri, + logger, + dartExecutable, + // Manually pass in a compiler. + cCompilerConfig: CCompilerConfig( + ar: Platform.environment[arKey]?.fileUri, + cc: cc, + envScript: Platform.environment[envScriptKey]?.fileUri, + envScriptArgs: Platform.environment[envScriptArgsKey]?.split(' '), + ld: Platform.environment[ldKey]?.fileUri, + ), + // Prevent any other environment variables. + includeParentEnvironment: false, + ); + expect(result.assets.length, 1); + }); + }); +} + +extension on String { + Uri get fileUri => Uri.file(this); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/build_runner_test.dart b/pkgs/native_assets_builder/test/build_runner/build_runner_test.dart new file mode 100644 index 0000000..f6fff2c --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/build_runner_test.dart
@@ -0,0 +1,63 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_builder/native_assets_builder.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('native_add build', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + // Trigger a build, should invoke build for libraries with native assets. + { + final logMessages = <String>[]; + final result = await build(packageUri, logger, dartExecutable, + capturedLogs: logMessages); + expect( + logMessages.join('\n'), + stringContainsInOrder( + ['native_add${Platform.pathSeparator}build.dart'])); + expect(result.assets.length, 1); + } + + // Trigger a build, should not invoke anything. + for (final passPackageLayout in [true, false]) { + PackageLayout? packageLayout; + if (passPackageLayout) { + packageLayout = await PackageLayout.fromRootPackageRoot(packageUri); + } + final logMessages = <String>[]; + final result = await build( + packageUri, + logger, + dartExecutable, + capturedLogs: logMessages, + packageLayout: packageLayout, + ); + expect( + false, + logMessages + .join('\n') + .contains('native_add${Platform.pathSeparator}build.dart'), + ); + expect(result.assets.length, 1); + } + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/helpers.dart b/pkgs/native_assets_builder/test/build_runner/helpers.dart new file mode 100644 index 0000000..9609c1c --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/helpers.dart
@@ -0,0 +1,138 @@ +// 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:file_testing/file_testing.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_builder/native_assets_builder.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +Future<void> runPubGet({ + required Uri workingDirectory, + required Logger logger, +}) async { + final result = await runProcess( + executable: Uri.file(Platform.resolvedExecutable), + arguments: [ + 'pub', + '--suppress-analytics', // Prevent extra log entries. + 'get', + ], + workingDirectory: workingDirectory, + logger: logger, + ); + expect(result.exitCode, 0); +} + +Future<BuildResult> build( + Uri packageUri, + Logger logger, + Uri dartExecutable, { + LinkModePreference linkModePreference = LinkModePreference.dynamic, + CCompilerConfig? cCompilerConfig, + bool includeParentEnvironment = true, + List<String>? capturedLogs, + PackageLayout? packageLayout, +}) async { + StreamSubscription<LogRecord>? subscription; + if (capturedLogs != null) { + subscription = + logger.onRecord.listen((event) => capturedLogs.add(event.message)); + } + + final result = await NativeAssetsBuildRunner( + logger: logger, + dartExecutable: dartExecutable, + ).build( + buildMode: BuildMode.release, + linkModePreference: linkModePreference, + target: Target.current, + workingDirectory: packageUri, + cCompilerConfig: cCompilerConfig, + includeParentEnvironment: includeParentEnvironment, + packageLayout: packageLayout, + ); + if (result.success) { + await expectAssetsExist(result.assets); + } + + if (subscription != null) { + await subscription.cancel(); + } + + return result; +} + +Future<DryRunResult> dryRun( + Uri packageUri, + Logger logger, + Uri dartExecutable, { + LinkModePreference linkModePreference = LinkModePreference.dynamic, + CCompilerConfig? cCompilerConfig, + bool includeParentEnvironment = true, + List<String>? capturedLogs, + PackageLayout? packageLayout, +}) async { + StreamSubscription<LogRecord>? subscription; + if (capturedLogs != null) { + subscription = + logger.onRecord.listen((event) => capturedLogs.add(event.message)); + } + + final result = await NativeAssetsBuildRunner( + logger: logger, + dartExecutable: dartExecutable, + ).dryRun( + linkModePreference: linkModePreference, + targetOs: Target.current.os, + workingDirectory: packageUri, + includeParentEnvironment: includeParentEnvironment, + packageLayout: packageLayout, + ); + + if (subscription != null) { + await subscription.cancel(); + } + + return result; +} + +Future<void> expectAssetsExist(List<Asset> assets) async { + for (final asset in assets) { + final uri = (asset.path as AssetAbsolutePath).uri; + expect( + uri.toFilePath(), + contains('${Platform.pathSeparator}.dart_tool${Platform.pathSeparator}' + 'native_assets_builder${Platform.pathSeparator}')); + final file = File.fromUri(uri); + expect(file, exists); + } +} + +Future<void> expectSymbols({ + required Asset asset, + required List<String> symbols, +}) async { + if (Platform.isLinux) { + final assetUri = (asset.path as AssetAbsolutePath).uri; + final nmResult = await runProcess( + executable: Uri(path: 'nm'), + arguments: [ + '-D', + assetUri.toFilePath(), + ], + logger: logger, + ); + + expect( + nmResult.stdout, + stringContainsInOrder(symbols), + ); + } +}
diff --git a/pkgs/native_assets_builder/test/build_runner/metadata_test.dart b/pkgs/native_assets_builder/test/build_runner/metadata_test.dart new file mode 100644 index 0000000..12e9461 --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/metadata_test.dart
@@ -0,0 +1,41 @@ +// 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. + +import 'dart:io'; + +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('get dependency metadata', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('package_reading_metadata/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + // Trigger a build, should invoke build for libraries with native assets. + { + final logMessages = <String>[]; + await build(packageUri, logger, dartExecutable, + capturedLogs: logMessages); + expect( + logMessages.join('\n'), + stringContainsInOrder([ + 'package_with_metadata${Platform.pathSeparator}build.dart', + 'package_reading_metadata${Platform.pathSeparator}build.dart', + '{some_int: 3, some_key: some_value}', + ])); + } + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/package_layout_test.dart b/pkgs/native_assets_builder/test/build_runner/package_layout_test.dart new file mode 100644 index 0000000..a92992f --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/package_layout_test.dart
@@ -0,0 +1,29 @@ +// 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. + +import 'package:native_assets_builder/native_assets_builder.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +void main() async { + test('fromRootPackageRoot', () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final nativeAddUri = tempUri.resolve('native_add/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet(workingDirectory: nativeAddUri, logger: logger); + + final packageLayout = + await PackageLayout.fromRootPackageRoot(nativeAddUri); + final packageLayout2 = PackageLayout.fromPackageConfig( + packageLayout.packageConfig, + packageLayout.packageConfigUri, + ); + expect(packageLayout.rootPackageRoot, packageLayout2.rootPackageRoot); + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/build_runner/packaging_preference_test.dart b/pkgs/native_assets_builder/test/build_runner/packaging_preference_test.dart new file mode 100644 index 0000000..e1fefee --- /dev/null +++ b/pkgs/native_assets_builder/test/build_runner/packaging_preference_test.dart
@@ -0,0 +1,60 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'helpers.dart'; + +const Timeout longTimeout = Timeout(Duration(minutes: 5)); + +void main() async { + test('link mode preference', timeout: longTimeout, () async { + await inTempDir((tempUri) async { + await copyTestProjects(targetUri: tempUri); + final packageUri = tempUri.resolve('native_add/'); + + // First, run `pub get`, we need pub to resolve our dependencies. + await runPubGet( + workingDirectory: packageUri, + logger: logger, + ); + + final resultDynamic = await build( + packageUri, + logger, + dartExecutable, + linkModePreference: LinkModePreference.dynamic, + ); + + final resultPreferDynamic = await build( + packageUri, + logger, + dartExecutable, + linkModePreference: LinkModePreference.preferDynamic, + ); + + final resultStatic = await build( + packageUri, + logger, + dartExecutable, + linkModePreference: LinkModePreference.static, + ); + + final resultPreferStatic = await build( + packageUri, + logger, + dartExecutable, + linkModePreference: LinkModePreference.preferStatic, + ); + + // This package honors preferences. + expect(resultDynamic.assets.single.linkMode, LinkMode.dynamic); + expect(resultPreferDynamic.assets.single.linkMode, LinkMode.dynamic); + expect(resultStatic.assets.single.linkMode, LinkMode.static); + expect(resultPreferStatic.assets.single.linkMode, LinkMode.static); + }); + }); +}
diff --git a/pkgs/native_assets_builder/test/data/README.md b/pkgs/native_assets_builder/test/data/README.md new file mode 100644 index 0000000..311839a --- /dev/null +++ b/pkgs/native_assets_builder/test/data/README.md
@@ -0,0 +1,6 @@ +The `dart_app` depends on multiple packages with native assets. + +`manifest.yaml` contains a list of all the files of the test projects. +This is used to copy the test projects to temporary folders when running tests, +to prevent tests accidentally using temporary files or interferring with each +other.
diff --git a/pkgs/native_assets_builder/test/data/cyclic_package_1/build.dart b/pkgs/native_assets_builder/test/data/cyclic_package_1/build.dart new file mode 100644 index 0000000..2479e4e --- /dev/null +++ b/pkgs/native_assets_builder/test/data/cyclic_package_1/build.dart
@@ -0,0 +1,11 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_builder/test/data/cyclic_package_1/pubspec.yaml b/pkgs/native_assets_builder/test/data/cyclic_package_1/pubspec.yaml new file mode 100644 index 0000000..ad3302f --- /dev/null +++ b/pkgs/native_assets_builder/test/data/cyclic_package_1/pubspec.yaml
@@ -0,0 +1,19 @@ +name: cyclic_package_1 +description: Part of two packages with native assets that have a cycle. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + cyclic_package_2: + path: ../cyclic_package_2 + native_assets_cli: ^0.3.0 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/data/cyclic_package_2/build.dart b/pkgs/native_assets_builder/test/data/cyclic_package_2/build.dart new file mode 100644 index 0000000..2479e4e --- /dev/null +++ b/pkgs/native_assets_builder/test/data/cyclic_package_2/build.dart
@@ -0,0 +1,11 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_builder/test/data/cyclic_package_2/pubspec.yaml b/pkgs/native_assets_builder/test/data/cyclic_package_2/pubspec.yaml new file mode 100644 index 0000000..1bf06f4 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/cyclic_package_2/pubspec.yaml
@@ -0,0 +1,19 @@ +name: cyclic_package_2 +description: Part of two packages with native assets that have a cycle. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + cyclic_package_1: + path: ../cyclic_package_1 + native_assets_cli: ^0.3.0 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/data/dart_app/.gitignore b/pkgs/native_assets_builder/test/data/dart_app/.gitignore new file mode 100644 index 0000000..2a9e6b8 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/dart_app/.gitignore
@@ -0,0 +1,2 @@ +.dart_tool +bin/dart_app/
diff --git a/pkgs/native_assets_builder/test/data/dart_app/bin/dart_app.dart b/pkgs/native_assets_builder/test/data/dart_app/bin/dart_app.dart new file mode 100644 index 0000000..4e9dc84 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/dart_app/bin/dart_app.dart
@@ -0,0 +1,27 @@ +// 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. + +import 'package:native_add/native_add.dart'; +import 'package:native_subtract/native_subtract.dart'; + +void main() { + testNativeAdd(); + testNativeSubtract(); +} + +void testNativeAdd() { + final answer = add(5, 6); + if (answer != 5 + 6) { + throw 'Wrong answer'; + } + print('add(5, 6) = $answer'); +} + +void testNativeSubtract() { + final answer = subtract(5, 6); + if (answer != 5 - 6) { + throw 'Wrong answer'; + } + print('subtract(5, 6) = $answer'); +}
diff --git a/pkgs/native_assets_builder/test/data/dart_app/pubspec.yaml b/pkgs/native_assets_builder/test/data/dart_app/pubspec.yaml new file mode 100644 index 0000000..1ead5cb --- /dev/null +++ b/pkgs/native_assets_builder/test/data/dart_app/pubspec.yaml
@@ -0,0 +1,12 @@ +name: dart_app + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + native_add: + path: ../native_add + native_subtract: + path: ../native_subtract
diff --git a/pkgs/native_assets_builder/test/data/manifest.yaml b/pkgs/native_assets_builder/test/data/manifest.yaml new file mode 100644 index 0000000..c52f500 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/manifest.yaml
@@ -0,0 +1,37 @@ +# The list of files to copy to a temporary folder to ensure running tests from +# a completely clean setup. +- cyclic_package_1/build.dart +- cyclic_package_1/pubspec.yaml +- cyclic_package_2/build.dart +- cyclic_package_2/pubspec.yaml +- dart_app/bin/dart_app.dart +- dart_app/pubspec.yaml +- native_add/build.dart +- native_add/ffigen.yaml +- native_add/lib/native_add.dart +- native_add/lib/src/native_add_bindings_generated.dart +- native_add/lib/src/native_add.dart +- native_add/pubspec.yaml +- native_add/src/native_add.c +- native_add/src/native_add.h +- native_add/test/native_add_test.dart +- native_subtract/build.dart +- native_subtract/ffigen.yaml +- native_subtract/lib/native_subtract.dart +- native_subtract/lib/src/native_subtract_bindings_generated.dart +- native_subtract/lib/src/native_subtract.dart +- native_subtract/pubspec.yaml +- native_subtract/src/native_subtract.c +- native_subtract/src/native_subtract.h +- package_reading_metadata/build.dart +- package_reading_metadata/pubspec.yaml +- package_with_metadata/build.dart +- package_with_metadata/pubspec.yaml +- wrong_build_output/build.dart +- wrong_build_output/pubspec.yaml +- wrong_build_output_2/build.dart +- wrong_build_output_2/pubspec.yaml +- wrong_build_output_3/build.dart +- wrong_build_output_3/pubspec.yaml +- wrong_namespace_asset/build.dart +- wrong_namespace_asset/pubspec.yaml
diff --git a/pkgs/native_assets_builder/test/data/native_add/build.dart b/pkgs/native_assets_builder/test/data/native_add/build.dart new file mode 100644 index 0000000..69bbb9e --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/build.dart
@@ -0,0 +1,31 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +const packageName = 'native_add'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.library( + name: packageName, + assetId: 'package:$packageName/src/${packageName}_bindings_generated.dart', + sources: [ + 'src/$packageName.c', + ], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: Logger('') + ..level = Level.ALL + ..onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }), + ); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_builder/test/data/native_add/ffigen.yaml b/pkgs/native_assets_builder/test/data/native_add/ffigen.yaml new file mode 100644 index 0000000..b8716bd --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/ffigen.yaml
@@ -0,0 +1,20 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: NativeAddBindings +description: | + Bindings for `src/native_add.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: "lib/src/native_add_bindings_generated.dart" +headers: + entry-points: + - "src/native_add.h" + include-directives: + - "src/native_add.h" +preamble: | + // 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. +comments: + style: any + length: full +ffi-native:
diff --git a/pkgs/native_assets_builder/test/data/native_add/lib/native_add.dart b/pkgs/native_assets_builder/test/data/native_add/lib/native_add.dart new file mode 100644 index 0000000..9eda681 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/lib/native_add.dart
@@ -0,0 +1,5 @@ +// 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. + +export 'src/native_add.dart';
diff --git a/pkgs/native_assets_builder/test/data/native_add/lib/src/native_add.dart b/pkgs/native_assets_builder/test/data/native_add/lib/src/native_add.dart new file mode 100644 index 0000000..9c8a43b --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/lib/src/native_add.dart
@@ -0,0 +1,7 @@ +// 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. + +import 'native_add_bindings_generated.dart' as bindings; + +int add(int a, int b) => bindings.add(a, b);
diff --git a/pkgs/native_assets_builder/test/data/native_add/lib/src/native_add_bindings_generated.dart b/pkgs/native_assets_builder/test/data/native_add/lib/src/native_add_bindings_generated.dart new file mode 100644 index 0000000..4d1e803 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/lib/src/native_add_bindings_generated.dart
@@ -0,0 +1,15 @@ +// 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<ffi.Int32 Function(ffi.Int32, ffi.Int32)>(symbol: 'add') +external int add( + int a, + int b, +);
diff --git a/pkgs/native_assets_builder/test/data/native_add/manifest.yaml b/pkgs/native_assets_builder/test/data/native_add/manifest.yaml new file mode 100644 index 0000000..e922db8 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/manifest.yaml
@@ -0,0 +1,9 @@ +- build.dart +- ffigen.yaml +- lib/native_add.dart +- lib/src/native_add_bindings_generated.dart +- lib/src/native_add.dart +- pubspec.yaml +- src/native_add.c +- src/native_add.h +- test/native_add_test.dart
diff --git a/pkgs/native_assets_builder/test/data/native_add/pubspec.yaml b/pkgs/native_assets_builder/test/data/native_add/pubspec.yaml new file mode 100644 index 0000000..67632a6 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/pubspec.yaml
@@ -0,0 +1,19 @@ +name: native_add +description: Sums two numbers with native code. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + logging: ^1.1.1 + native_assets_cli: ^0.3.0 + native_toolchain_c: ^0.3.0 + +dev_dependencies: + ffigen: ^8.0.2 + lints: ^3.0.0 + test: ^1.23.1
diff --git a/pkgs/native_assets_builder/test/data/native_add/src/native_add.c b/pkgs/native_assets_builder/test/data/native_add/src/native_add.c new file mode 100644 index 0000000..cf21076 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/src/native_add.c
@@ -0,0 +1,9 @@ +// 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. + +#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/native_add/src/native_add.h b/pkgs/native_assets_builder/test/data/native_add/src/native_add.h new file mode 100644 index 0000000..5824f98 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/src/native_add.h
@@ -0,0 +1,13 @@ +// 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. + +#include <stdint.h> + +#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/native_add/test/native_add_test.dart b/pkgs/native_assets_builder/test/data/native_add/test/native_add_test.dart new file mode 100644 index 0000000..7531a62 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add/test/native_add_test.dart
@@ -0,0 +1,13 @@ +// 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. + +import 'package:native_add/native_add.dart'; +import 'package:test/test.dart'; + +void main() { + test('native add test', () { + final result = add(4, 6); + expect(result, equals(10)); + }); +}
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_source/README.md b/pkgs/native_assets_builder/test/data/native_add_add_source/README.md new file mode 100644 index 0000000..5a1b807 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_source/README.md
@@ -0,0 +1 @@ +Used in automated testing to modify the native_add project.
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_source/build.dart b/pkgs/native_assets_builder/test/data/native_add_add_source/build.dart new file mode 100644 index 0000000..3fcb30e --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_source/build.dart
@@ -0,0 +1,32 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +const packageName = 'native_add'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.library( + name: packageName, + assetId: 'package:$packageName/src/${packageName}_bindings_generated.dart', + sources: [ + 'src/$packageName.c', + 'src/native_multiply.c', + ], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: Logger('') + ..level = Level.ALL + ..onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }), + ); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_source/manifest.yaml b/pkgs/native_assets_builder/test/data/native_add_add_source/manifest.yaml new file mode 100644 index 0000000..3a2f6c3 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_source/manifest.yaml
@@ -0,0 +1,3 @@ +- build.dart +- src/native_multiply.c +- src/native_multiply.h
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_source/pubspec.yaml b/pkgs/native_assets_builder/test/data/native_add_add_source/pubspec.yaml new file mode 100644 index 0000000..67632a6 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_source/pubspec.yaml
@@ -0,0 +1,19 @@ +name: native_add +description: Sums two numbers with native code. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + logging: ^1.1.1 + native_assets_cli: ^0.3.0 + native_toolchain_c: ^0.3.0 + +dev_dependencies: + ffigen: ^8.0.2 + lints: ^3.0.0 + test: ^1.23.1
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_source/src/native_multiply.c b/pkgs/native_assets_builder/test/data/native_add_add_source/src/native_multiply.c new file mode 100644 index 0000000..7801f65 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_source/src/native_multiply.c
@@ -0,0 +1,9 @@ +// 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. + +#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/native_add_add_source/src/native_multiply.h b/pkgs/native_assets_builder/test/data/native_add_add_source/src/native_multiply.h new file mode 100644 index 0000000..0827571 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_source/src/native_multiply.h
@@ -0,0 +1,13 @@ +// 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. + +#include <stdint.h> + +#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/native_add_add_symbol/README.md b/pkgs/native_assets_builder/test/data/native_add_add_symbol/README.md new file mode 100644 index 0000000..5a1b807 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_symbol/README.md
@@ -0,0 +1 @@ +Used in automated testing to modify the native_add project.
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_symbol/manifest.yaml b/pkgs/native_assets_builder/test/data/native_add_add_symbol/manifest.yaml new file mode 100644 index 0000000..adc0539 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_symbol/manifest.yaml
@@ -0,0 +1,2 @@ +- src/native_add.c +- src/native_add.h
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_symbol/src/native_add.c b/pkgs/native_assets_builder/test/data/native_add_add_symbol/src/native_add.c new file mode 100644 index 0000000..a493add --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_symbol/src/native_add.c
@@ -0,0 +1,14 @@ +// 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. + +#include "native_add.h" + +MYLIB_EXPORT int32_t add(int32_t a, int32_t b) { + return a + b; +} + + +MYLIB_EXPORT intptr_t subtract(intptr_t a, intptr_t b) { + return a - b; +}
diff --git a/pkgs/native_assets_builder/test/data/native_add_add_symbol/src/native_add.h b/pkgs/native_assets_builder/test/data/native_add_add_symbol/src/native_add.h new file mode 100644 index 0000000..37d53b6 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_add_symbol/src/native_add.h
@@ -0,0 +1,15 @@ +// 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. + +#include <stdint.h> + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT int32_t add(int32_t a, int32_t b); + +MYLIB_EXPORT intptr_t subtract(intptr_t a, intptr_t b);
diff --git a/pkgs/native_assets_builder/test/data/native_add_break_build/README.md b/pkgs/native_assets_builder/test/data/native_add_break_build/README.md new file mode 100644 index 0000000..5a1b807 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_break_build/README.md
@@ -0,0 +1 @@ +Used in automated testing to modify the native_add project.
diff --git a/pkgs/native_assets_builder/test/data/native_add_break_build/manifest.yaml b/pkgs/native_assets_builder/test/data/native_add_break_build/manifest.yaml new file mode 100644 index 0000000..a475918 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_break_build/manifest.yaml
@@ -0,0 +1 @@ +- src/native_add.c
diff --git a/pkgs/native_assets_builder/test/data/native_add_break_build/src/native_add.c b/pkgs/native_assets_builder/test/data/native_add_break_build/src/native_add.c new file mode 100644 index 0000000..ba06d8b --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_break_build/src/native_add.c
@@ -0,0 +1,9 @@ +// 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. + +#include "native_add.h" + +MYLIB_EXPORT int32_t add(int32_t different_parameters) { + return a + b; +} \ No newline at end of file
diff --git a/pkgs/native_assets_builder/test/data/native_add_fix_build/README.md b/pkgs/native_assets_builder/test/data/native_add_fix_build/README.md new file mode 100644 index 0000000..5a1b807 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_fix_build/README.md
@@ -0,0 +1 @@ +Used in automated testing to modify the native_add project.
diff --git a/pkgs/native_assets_builder/test/data/native_add_fix_build/manifest.yaml b/pkgs/native_assets_builder/test/data/native_add_fix_build/manifest.yaml new file mode 100644 index 0000000..a475918 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_fix_build/manifest.yaml
@@ -0,0 +1 @@ +- src/native_add.c
diff --git a/pkgs/native_assets_builder/test/data/native_add_fix_build/src/native_add.c b/pkgs/native_assets_builder/test/data/native_add_fix_build/src/native_add.c new file mode 100644 index 0000000..cf21076 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_add_fix_build/src/native_add.c
@@ -0,0 +1,9 @@ +// 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. + +#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/native_subtract/build.dart b/pkgs/native_assets_builder/test/data/native_subtract/build.dart new file mode 100644 index 0000000..66b62ea --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/build.dart
@@ -0,0 +1,31 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +const packageName = 'native_subtract'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.library( + name: packageName, + assetId: 'package:$packageName/src/${packageName}_bindings_generated.dart', + sources: [ + 'src/$packageName.c', + ], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: Logger('') + ..level = Level.ALL + ..onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); + }), + ); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_builder/test/data/native_subtract/ffigen.yaml b/pkgs/native_assets_builder/test/data/native_subtract/ffigen.yaml new file mode 100644 index 0000000..5f79dbe --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/ffigen.yaml
@@ -0,0 +1,20 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: NativeAddBindings +description: | + Bindings for `src/native_subtract.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: "lib/src/native_subtract_bindings_generated.dart" +headers: + entry-points: + - "src/native_subtract.h" + include-directives: + - "src/native_subtract.h" +preamble: | + // 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. +comments: + style: any + length: full +ffi-native:
diff --git a/pkgs/native_assets_builder/test/data/native_subtract/lib/native_subtract.dart b/pkgs/native_assets_builder/test/data/native_subtract/lib/native_subtract.dart new file mode 100644 index 0000000..a283156 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/lib/native_subtract.dart
@@ -0,0 +1,5 @@ +// 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. + +export 'src/native_subtract.dart';
diff --git a/pkgs/native_assets_builder/test/data/native_subtract/lib/src/native_subtract.dart b/pkgs/native_assets_builder/test/data/native_subtract/lib/src/native_subtract.dart new file mode 100644 index 0000000..3d77e75 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/lib/src/native_subtract.dart
@@ -0,0 +1,7 @@ +// 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. + +import 'native_subtract_bindings_generated.dart' as bindings; + +int subtract(int a, int b) => bindings.subtract(a, b);
diff --git a/pkgs/native_assets_builder/test/data/native_subtract/lib/src/native_subtract_bindings_generated.dart b/pkgs/native_assets_builder/test/data/native_subtract/lib/src/native_subtract_bindings_generated.dart new file mode 100644 index 0000000..1a952fe --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/lib/src/native_subtract_bindings_generated.dart
@@ -0,0 +1,15 @@ +// 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<ffi.Int32 Function(ffi.Int32, ffi.Int32)>(symbol: 'subtract') +external int subtract( + int a, + int b, +);
diff --git a/pkgs/native_assets_builder/test/data/native_subtract/pubspec.yaml b/pkgs/native_assets_builder/test/data/native_subtract/pubspec.yaml new file mode 100644 index 0000000..de93ce0 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/pubspec.yaml
@@ -0,0 +1,19 @@ +name: native_subtract +description: Subtracts two numbers with native code. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + logging: ^1.1.1 + native_assets_cli: ^0.3.0 + native_toolchain_c: ^0.3.0 + +dev_dependencies: + ffigen: ^8.0.2 + lints: ^3.0.0 + test: ^1.23.1
diff --git a/pkgs/native_assets_builder/test/data/native_subtract/src/native_subtract.c b/pkgs/native_assets_builder/test/data/native_subtract/src/native_subtract.c new file mode 100644 index 0000000..6562ae6 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/src/native_subtract.c
@@ -0,0 +1,9 @@ +// 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. + +#include "native_subtract.h" + +int32_t subtract(int32_t a, int32_t b) { + return a - b; +}
diff --git a/pkgs/native_assets_builder/test/data/native_subtract/src/native_subtract.h b/pkgs/native_assets_builder/test/data/native_subtract/src/native_subtract.h new file mode 100644 index 0000000..407fc5d --- /dev/null +++ b/pkgs/native_assets_builder/test/data/native_subtract/src/native_subtract.h
@@ -0,0 +1,13 @@ +// 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. + +#include <stdint.h> + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT int32_t subtract(int32_t a, int32_t b);
diff --git a/pkgs/native_assets_builder/test/data/package_reading_metadata/build.dart b/pkgs/native_assets_builder/test/data/package_reading_metadata/build.dart new file mode 100644 index 0000000..e8009db --- /dev/null +++ b/pkgs/native_assets_builder/test/data/package_reading_metadata/build.dart
@@ -0,0 +1,22 @@ +// 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. + +import 'package:cli_config/cli_config.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final config = await Config.fromArgs(args: args); + final buildConfig = BuildConfig.fromConfig(config); + if (!buildConfig.dryRun) { + final metadata = + buildConfig.dependencyMetadata!['package_with_metadata']!.metadata; + final someValue = metadata['some_key']; + assert(someValue != null); + final someInt = metadata['some_int']; + assert(someInt != null); + print(metadata); + } else { + print('meta data not available in dry run'); + } +}
diff --git a/pkgs/native_assets_builder/test/data/package_reading_metadata/pubspec.yaml b/pkgs/native_assets_builder/test/data/package_reading_metadata/pubspec.yaml new file mode 100644 index 0000000..43f2318 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/package_reading_metadata/pubspec.yaml
@@ -0,0 +1,19 @@ +name: package_reading_metadata +description: Reads some metadata in its build.dart +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + native_assets_cli: ^0.3.0 + package_with_metadata: + path: ../package_with_metadata/ + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/data/package_with_metadata/build.dart b/pkgs/native_assets_builder/test/data/package_with_metadata/build.dart new file mode 100644 index 0000000..a5e908a --- /dev/null +++ b/pkgs/native_assets_builder/test/data/package_with_metadata/build.dart
@@ -0,0 +1,16 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput( + metadata: Metadata({ + 'some_key': 'some_value', + 'some_int': 3, + }), + ); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_builder/test/data/package_with_metadata/pubspec.yaml b/pkgs/native_assets_builder/test/data/package_with_metadata/pubspec.yaml new file mode 100644 index 0000000..bca9ddd --- /dev/null +++ b/pkgs/native_assets_builder/test/data/package_with_metadata/pubspec.yaml
@@ -0,0 +1,17 @@ +name: package_with_metadata +description: Sets some metadata in its build.dart +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + native_assets_cli: ^0.3.0 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/data/wrong_build_output/build.dart b/pkgs/native_assets_builder/test/data/wrong_build_output/build.dart new file mode 100644 index 0000000..a34dbd0 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_build_output/build.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. + +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + await File.fromUri(buildConfig.outDir.resolve(BuildOutput.fileName)) + .writeAsString(_wrongContents); +} + +const _wrongContents = ''' +timestamp: 2023-07-28 14:22:45.000 +assets: [] +dependencies: [] +metadata: {} +version: 9001.0.0 +''';
diff --git a/pkgs/native_assets_builder/test/data/wrong_build_output/pubspec.yaml b/pkgs/native_assets_builder/test/data/wrong_build_output/pubspec.yaml new file mode 100644 index 0000000..e6b5419 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_build_output/pubspec.yaml
@@ -0,0 +1,17 @@ +name: wrong_build_output +description: Package that tries to add an asset with an id not prefixed by its package name. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + native_assets_cli: ^0.3.0 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/data/wrong_build_output_2/build.dart b/pkgs/native_assets_builder/test/data/wrong_build_output_2/build.dart new file mode 100644 index 0000000..331cd2d --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_build_output_2/build.dart
@@ -0,0 +1,22 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + await File.fromUri(buildConfig.outDir.resolve(BuildOutput.fileName)) + .writeAsString(_wrongContents); +} + +const _wrongContents = ''' +timestamp: 2023-07-28 14:22:45.000 +assets: + foo: 123 +dependencies: [] +metadata: {} +version: 1.0.0 +''';
diff --git a/pkgs/native_assets_builder/test/data/wrong_build_output_2/pubspec.yaml b/pkgs/native_assets_builder/test/data/wrong_build_output_2/pubspec.yaml new file mode 100644 index 0000000..9e87ed9 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_build_output_2/pubspec.yaml
@@ -0,0 +1,17 @@ +name: wrong_build_output_2 +description: Package that tries to add an asset with an id not prefixed by its package name. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + native_assets_cli: ^0.3.0 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/data/wrong_build_output_3/build.dart b/pkgs/native_assets_builder/test/data/wrong_build_output_3/build.dart new file mode 100644 index 0000000..307dfff --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_build_output_3/build.dart
@@ -0,0 +1,22 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + await File.fromUri(buildConfig.outDir.resolve(BuildOutput.fileName)) + .writeAsString(_rightContents); + exit(1); +} + +const _rightContents = ''' +timestamp: 2023-07-28 14:22:45.000 +assets: [] +dependencies: [] +metadata: {} +version: 1.0.0 +''';
diff --git a/pkgs/native_assets_builder/test/data/wrong_build_output_3/pubspec.yaml b/pkgs/native_assets_builder/test/data/wrong_build_output_3/pubspec.yaml new file mode 100644 index 0000000..1cdc5b2 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_build_output_3/pubspec.yaml
@@ -0,0 +1,17 @@ +name: wrong_build_output_3 +description: Package that outputs a valid build_output.yaml but a non-zero exit code. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + native_assets_cli: ^0.3.0 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/data/wrong_namespace_asset/build.dart b/pkgs/native_assets_builder/test/data/wrong_namespace_asset/build.dart new file mode 100644 index 0000000..457aa90 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_namespace_asset/build.dart
@@ -0,0 +1,24 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; + +void main(List<String> args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput( + assets: [ + Asset( + id: 'package:other_package/foo', + linkMode: LinkMode.dynamic, + target: Target.current, + path: AssetAbsolutePath( + buildConfig.outDir.resolve( + Target.current.os.dylibFileName('foo'), + ), + ), + ), + ], + ); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_builder/test/data/wrong_namespace_asset/pubspec.yaml b/pkgs/native_assets_builder/test/data/wrong_namespace_asset/pubspec.yaml new file mode 100644 index 0000000..fc6b9e8 --- /dev/null +++ b/pkgs/native_assets_builder/test/data/wrong_namespace_asset/pubspec.yaml
@@ -0,0 +1,17 @@ +name: wrong_namespace_asset +description: Package that tries to add an asset with an id not prefixed by its package name. +version: 0.1.0 + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + native_assets_cli: ^0.3.0 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + lints: ^3.0.0
diff --git a/pkgs/native_assets_builder/test/helpers.dart b/pkgs/native_assets_builder/test/helpers.dart new file mode 100644 index 0000000..522aa97 --- /dev/null +++ b/pkgs/native_assets_builder/test/helpers.dart
@@ -0,0 +1,188 @@ +// 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_builder/src/utils/run_process.dart' + as run_process; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +extension UriExtension on Uri { + Uri get parent => File(toFilePath()).parent.uri; +} + +const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; + +Future<void> inTempDir( + Future<void> Function(Uri tempUri) fun, { + String? prefix, + bool keepTemp = false, +}) async { + final tempDir = await Directory.systemTemp.createTemp(prefix); + // Deal with Windows temp folder aliases. + final tempUri = + Directory(await tempDir.resolveSymbolicLinks()).uri.normalizePath(); + try { + await fun(tempUri); + } finally { + if ((!Platform.environment.containsKey(keepTempKey) || + Platform.environment[keepTempKey]!.isEmpty) && + !keepTemp) { + await tempDir.delete(recursive: true); + } + } +} + +/// Runs a [Process]. +/// +/// If [logger] is provided, stream stdout and stderr to it. +/// +/// If [captureOutput], captures stdout and stderr. +Future<run_process.RunProcessResult> runProcess({ + required Uri executable, + List<String> arguments = const [], + Uri? workingDirectory, + Map<String, String>? environment, + bool includeParentEnvironment = true, + required Logger? logger, + bool captureOutput = true, + int expectedExitCode = 0, + bool throwOnUnexpectedExitCode = false, +}) => + run_process.runProcess( + executable: executable, + arguments: arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + logger: logger, + captureOutput: captureOutput, + expectedExitCode: expectedExitCode, + throwOnUnexpectedExitCode: throwOnUnexpectedExitCode, + ); + +/// Test files are run in a variety of ways, find this package root in all. +/// +/// Test files can be run from source from any working directory. The Dart SDK +/// `tools/test.py` runs them from the root of the SDK for example. +/// +/// Test files can be run from dill from the root of package. `package:test` +/// does this. +/// +/// https://github.com/dart-lang/test/issues/110 +Uri findPackageRoot(String packageName) { + final script = Platform.script; + final fileName = script.name; + if (fileName.endsWith('_test.dart')) { + // We're likely running from source. + var directory = script.resolve('.'); + while (true) { + final dirName = directory.name; + if (dirName == packageName) { + return directory; + } + final parent = directory.resolve('..'); + if (parent == directory) break; + directory = parent; + } + } else if (fileName.endsWith('.dill')) { + final cwd = Directory.current.uri; + final dirName = cwd.name; + if (dirName == packageName) { + return cwd; + } + } + throw StateError("Could not find package root for package '$packageName'. " + 'Tried finding the package root via Platform.script ' + "'${Platform.script.toFilePath()}' and Directory.current " + "'${Directory.current.uri.toFilePath()}'."); +} + +final pkgNativeAssetsBuilderUri = findPackageRoot('native_assets_builder'); + +final testDataUri = pkgNativeAssetsBuilderUri.resolve('test/data/'); + +extension on Uri { + String get name => pathSegments.where((e) => e != '').last; +} + +Future<void> copyTestProjects({ + Uri? sourceUri, + required Uri targetUri, +}) async { + sourceUri ??= testDataUri; + final manifestUri = sourceUri.resolve('manifest.yaml'); + final manifestFile = File.fromUri(manifestUri); + final manifestString = await manifestFile.readAsString(); + final manifestYaml = loadYamlDocument(manifestString); + final manifest = [ + for (final path in manifestYaml.contents as YamlList) + Uri(path: path as String) + ]; + final filesToCopy = + manifest.where((e) => e.pathSegments.last != 'pubspec.yaml').toList(); + final filesToModify = + manifest.where((e) => e.pathSegments.last == 'pubspec.yaml').toList(); + + for (final pathToCopy in filesToCopy) { + final sourceFile = File.fromUri(sourceUri.resolveUri(pathToCopy)); + final targetFileUri = targetUri.resolveUri(pathToCopy); + final targetDirUri = targetFileUri.parent; + final targetDir = Directory.fromUri(targetDirUri); + if (!(await targetDir.exists())) { + await targetDir.create(recursive: true); + } + + // Copying files on MacOS and Windows preserves the source timestamps. + // The builder will use the cached build if the timestamps are equal. + // So just write the file instead. + final targetFile = File.fromUri(targetFileUri); + await targetFile.writeAsBytes(await sourceFile.readAsBytes()); + } + for (final pathToModify in filesToModify) { + final sourceFile = File.fromUri(sourceUri.resolveUri(pathToModify)); + final targetFileUri = targetUri.resolveUri(pathToModify); + final sourceString = await sourceFile.readAsString(); + final modifiedString = sourceString.replaceAll( + 'path: ../../../', + 'path: ${pkgNativeAssetsBuilderUri.toFilePath().replaceAll('\\', '/')}', + ); + await File.fromUri(targetFileUri) + .writeAsString(modifiedString, flush: true); + } +} + +/// Logger that outputs the full trace when a test fails. +Logger get logger => _logger ??= () { + // A new logger is lazily created for each test so that the messages + // captured by printOnFailure are scoped to the correct test. + addTearDown(() => _logger = null); + return _createTestLogger(); + }(); + +Logger? _logger; + +Logger createCapturingLogger( + List<String> capturedMessages, { + Level level = Level.ALL, +}) => + _createTestLogger(capturedMessages: capturedMessages, level: level); + +Logger _createTestLogger({ + List<String>? capturedMessages, + Level level = Level.ALL, +}) => + Logger.detached('') + ..level = level + ..onRecord.listen((record) { + printOnFailure( + '${record.level.name}: ${record.time}: ${record.message}', + ); + capturedMessages?.add(record.message); + }); + +final dartExecutable = File(Platform.resolvedExecutable).uri;
diff --git a/pkgs/native_assets_cli/.gitignore b/pkgs/native_assets_cli/.gitignore new file mode 100644 index 0000000..ef8a06e --- /dev/null +++ b/pkgs/native_assets_cli/.gitignore
@@ -0,0 +1,14 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +coverage/ + +*.dll +*.dylib +*.exe +*.so
diff --git a/pkgs/native_assets_cli/CHANGELOG.md b/pkgs/native_assets_cli/CHANGELOG.md new file mode 100644 index 0000000..0159aab --- /dev/null +++ b/pkgs/native_assets_cli/CHANGELOG.md
@@ -0,0 +1,26 @@ +## 0.3.2 + +- Fixed an issue where `Depenendencies.dependencies` could not be + modified when expected to. + +## 0.3.1 + +- Added `Target.androidRiscv64`. + +## 0.3.0 + +- **Breaking change** Add required `BuildConfig.packageName` + ([#142](https://github.com/dart-lang/native/issues/142)). + +## 0.2.0 + +- **Breaking change** Rename `Asset.name` to `Asset.id` + ([#100](https://github.com/dart-lang/native/issues/100)). +- Added topics. +- Fixed metadata example. +- Throws `FormatException`s instead of `TypeError`s when failing to parse Yaml + ([#109](https://github.com/dart-lang/native/issues/109)). + +## 0.1.0 + +- Initial version.
diff --git a/pkgs/native_assets_cli/LICENSE b/pkgs/native_assets_cli/LICENSE new file mode 100644 index 0000000..4fd5739 --- /dev/null +++ b/pkgs/native_assets_cli/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2023, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/native_assets_cli/README.md b/pkgs/native_assets_cli/README.md new file mode 100644 index 0000000..7a336e0 --- /dev/null +++ b/pkgs/native_assets_cli/README.md
@@ -0,0 +1,54 @@ +[](https://github.com/dart-lang/native/actions/workflows/native.yaml) +[](https://coveralls.io/github/dart-lang/native?branch=main) +[](https://pub.dev/packages/native_assets_cli) +[](https://pub.dev/packages/native_assets_cli/publisher) + +This library contains the CLI specification and a default implementation +for bundling native code with Dart packages. + +## Status: Experimental + +**NOTE**: This package is currently experimental and published under the +[labs.dart.dev](https://dart.dev/dart-team-packages) pub publisher in order to +solicit feedback. + +For packages in the labs.dart.dev publisher we generally plan to either graduate +the package into a supported publisher (dart.dev, tools.dart.dev) after a period +of feedback and iteration, or discontinue the package. These packages have a +much higher expected rate of API and breaking changes. + +Your feedback is valuable and will help us evolve this package. +For bugs, please file an issue in the +[bug tracker](https://github.com/dart-lang/native/issues). +For general feedback and suggestions for the native assets feature in Dart and +Flutter, please comment in [dart-lang#50565] or [flutter#129757]. + +## Example + +A typical layout of a package with native code is: + +* `lib/` contains Dart code which uses [`dart:ffi`] and [`package:ffigen`] + to call into native code. +* `src/` contains C/C++/Rust code which is invoked through `dart:ffi`. +* `build.dart` implements the CLI that communicates which native assets + to build/bundle with the Dart/Flutter SDK. This file uses the + protocol specified in this package. + +An example can be found in [example/](example/). + +## Usage + +Using the native assets feature requires passing +`--enable-experiment=native-assets` in Dart on a dev build. + +The native assets feature is not yet available in Flutter. + +## Development + +The development of the feature can be tracked in [dart-lang#50565], +[flutter#129757], and in the issue tracker on this repository. + +[`dart:ffi`]: https://api.dart.dev/stable/dart-ffi/dart-ffi-library.html +[`package:ffigen`]: https://pub.dev/packages/ffigen +[dart-lang#50565]: https://github.com/dart-lang/sdk/issues/50565 +[flutter#129757]: https://github.com/flutter/flutter/issues/129757
diff --git a/pkgs/native_assets_cli/analysis_options.yaml b/pkgs/native_assets_cli/analysis_options.yaml new file mode 100644 index 0000000..dd3dcda --- /dev/null +++ b/pkgs/native_assets_cli/analysis_options.yaml
@@ -0,0 +1,12 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-raw-types: true + +linter: + rules: + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_in_for_each + - prefer_final_locals
diff --git a/pkgs/native_assets_cli/example/README.md b/pkgs/native_assets_cli/example/README.md new file mode 100644 index 0000000..1be681f --- /dev/null +++ b/pkgs/native_assets_cli/example/README.md
@@ -0,0 +1,9 @@ +The examples in this folder illustrate how native code is built and bundled +in Dart and Flutter apps. + +* [native_add_app/](native_add_app/) has a dependency with C code. + This app should declare nothing special. Dart and Flutter should check + all dependencies for native code. +* [native_add_library/](native_add_library/) contains a library with C code. + When Dart code in this library or dependent on this library is invoked, the + C code must be built and bundled so that it can be used by the Dart code.
diff --git a/pkgs/native_assets_cli/example/native_add_app/README.md b/pkgs/native_assets_cli/example/native_add_app/README.md new file mode 100644 index 0000000..50a8bc0 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_app/README.md
@@ -0,0 +1,10 @@ +An example application to show invocation of native code bundled with the native +assets feature. + +## Usage + +Run application with `dart --enable-experiment=native-assets run`. + +Run tests with `dart --enable-experiment=native-assets test`. + +Release application with `dart --enable-experiment=native-assets build bin/native_add_app`. (Run with `./bin/native_add_app/native_add_app.exe`.)
diff --git a/pkgs/native_assets_cli/example/native_add_app/bin/native_add_app.dart b/pkgs/native_assets_cli/example/native_add_app/bin/native_add_app.dart new file mode 100644 index 0000000..40bd133 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_app/bin/native_add_app.dart
@@ -0,0 +1,11 @@ +// 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. + +import 'package:native_add_library/native_add_library.dart'; + +void main() { + print('Invoking a native function to calculate 1 + 2.'); + final result = add(1, 2); + print('Invocation success: 1 + 2 = $result.'); +}
diff --git a/pkgs/native_assets_cli/example/native_add_app/pubspec.yaml b/pkgs/native_assets_cli/example/native_add_app/pubspec.yaml new file mode 100644 index 0000000..68f3d8e --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_app/pubspec.yaml
@@ -0,0 +1,17 @@ +publish_to: none + +name: native_add_app +description: Invokes a package with native assets. +version: 0.1.0 +repository: https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli/example/native_add_app + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + native_add_library: + path: ../native_add_library/ + +dev_dependencies: + lints: ^3.0.0 + test: ^1.21.0
diff --git a/pkgs/native_assets_cli/example/native_add_app/test/native_add_library_test.dart b/pkgs/native_assets_cli/example/native_add_app/test/native_add_library_test.dart new file mode 100644 index 0000000..692446e --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_app/test/native_add_library_test.dart
@@ -0,0 +1,12 @@ +// 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. + +import 'package:native_add_library/native_add_library.dart'; +import 'package:test/test.dart'; + +void main() { + test('invoke native function', () { + expect(add(24, 18), 42); + }); +}
diff --git a/pkgs/native_assets_cli/example/native_add_library/README.md b/pkgs/native_assets_cli/example/native_add_library/README.md new file mode 100644 index 0000000..180cb02 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/README.md
@@ -0,0 +1,16 @@ +An example library containing native code that should be bundled with Dart and +Flutter applications. + +## Usage + +Run tests with `dart --enable-experiment=native-assets test`. + +## Code organization + +A typical layout of a package with native code is: + +* `lib/` contains Dart code which uses [`dart:ffi`] and [`package:ffigen`] + to call into native code. +* `src/` contains C code which is built and then invoked through `dart:ffi`. +* `build.dart` implements the CLI that communicates which native assets + to build/bundle with the Dart/Flutter SDK.
diff --git a/pkgs/native_assets_cli/example/native_add_library/build.dart b/pkgs/native_assets_cli/example/native_add_library/build.dart new file mode 100644 index 0000000..ae39cdc --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/build.dart
@@ -0,0 +1,39 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; + +const packageName = 'native_add_library'; + +/// Implements the protocol from `package:native_assets_cli` by building +/// the C code in `src/` and reporting what native assets it built. +void main(List<String> args) async { + // Parse the build configuration passed to this CLI from Dart or Flutter. + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + + // Configure `package:native_toolchain_c` to build the C code for us. + final cbuilder = CBuilder.library( + name: packageName, + assetId: 'package:$packageName/${packageName}.dart', + sources: [ + 'src/$packageName.c', + ], + ); + await cbuilder.run( + buildConfig: buildConfig, + // `package:native_toolchain_c` will output the dynamic or static libraries it built, + // what files it accessed (for caching the build), etc. + buildOutput: buildOutput, + logger: Logger('') + ..level = Level.ALL + ..onRecord.listen((record) => print(record.message)), + ); + + // Write the output according to the native assets protocol so that Dart or + // Flutter can find the native assets produced by this script. + await buildOutput.writeToFile(outDir: buildConfig.outDir); +}
diff --git a/pkgs/native_assets_cli/example/native_add_library/ffigen.yaml b/pkgs/native_assets_cli/example/native_add_library/ffigen.yaml new file mode 100644 index 0000000..4b32465 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/ffigen.yaml
@@ -0,0 +1,20 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: NativeAddBindings +description: | + Bindings for `src/native_add.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: 'lib/native_add_library.dart' +headers: + entry-points: + - 'src/native_add_library.h' + include-directives: + - 'src/native_add_library.h' +preamble: | + // 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. +comments: + style: any + length: full +ffi-native:
diff --git a/pkgs/native_assets_cli/example/native_add_library/lib/native_add_library.dart b/pkgs/native_assets_cli/example/native_add_library/lib/native_add_library.dart new file mode 100644 index 0000000..4d1e803 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/lib/native_add_library.dart
@@ -0,0 +1,15 @@ +// 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<ffi.Int32 Function(ffi.Int32, ffi.Int32)>(symbol: 'add') +external int add( + int a, + int b, +);
diff --git a/pkgs/native_assets_cli/example/native_add_library/pubspec.yaml b/pkgs/native_assets_cli/example/native_add_library/pubspec.yaml new file mode 100644 index 0000000..02f8219 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/pubspec.yaml
@@ -0,0 +1,20 @@ +publish_to: none + +name: native_add_library +description: Sums two numbers with native code. +version: 0.1.0 +repository: https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli/example/native_add_library + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + logging: ^1.1.1 + native_assets_cli: ^0.3.0 + native_toolchain_c: ^0.3.0 + +dev_dependencies: + ffigen: ^8.0.2 + lints: ^3.0.0 + test: ^1.21.0
diff --git a/pkgs/native_assets_cli/example/native_add_library/src/native_add_library.c b/pkgs/native_assets_cli/example/native_add_library/src/native_add_library.c new file mode 100644 index 0000000..cf68dd3 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/src/native_add_library.c
@@ -0,0 +1,16 @@ +// 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. + +#include "native_add_library.h" + +#ifdef DEBUG +#include <stdio.h> +#endif + +int32_t add(int32_t a, int32_t b) { +#ifdef DEBUG + printf("Adding %i and %i.\n", a, b); +#endif + return a + b; +}
diff --git a/pkgs/native_assets_cli/example/native_add_library/src/native_add_library.h b/pkgs/native_assets_cli/example/native_add_library/src/native_add_library.h new file mode 100644 index 0000000..5824f98 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/src/native_add_library.h
@@ -0,0 +1,13 @@ +// 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. + +#include <stdint.h> + +#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_cli/example/native_add_library/test/native_add_library_test.dart b/pkgs/native_assets_cli/example/native_add_library/test/native_add_library_test.dart new file mode 100644 index 0000000..692446e --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add_library/test/native_add_library_test.dart
@@ -0,0 +1,12 @@ +// 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. + +import 'package:native_add_library/native_add_library.dart'; +import 'package:test/test.dart'; + +void main() { + test('invoke native function', () { + expect(add(24, 18), 42); + }); +}
diff --git a/pkgs/native_assets_cli/lib/native_assets_cli.dart b/pkgs/native_assets_cli/lib/native_assets_cli.dart new file mode 100644 index 0000000..8ee3d30 --- /dev/null +++ b/pkgs/native_assets_cli/lib/native_assets_cli.dart
@@ -0,0 +1,18 @@ +// 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. + +/// A library that contains the argument and file formats for implementing a +/// native assets CLI. +library native_assets_cli; + +export 'src/model/asset.dart'; +export 'src/model/build_config.dart'; +export 'src/model/build_mode.dart'; +export 'src/model/build_output.dart'; +export 'src/model/dependencies.dart'; +export 'src/model/ios_sdk.dart'; +export 'src/model/link_mode.dart'; +export 'src/model/link_mode_preference.dart'; +export 'src/model/metadata.dart'; +export 'src/model/target.dart';
diff --git a/pkgs/native_assets_cli/lib/src/model/asset.dart b/pkgs/native_assets_cli/lib/src/model/asset.dart new file mode 100644 index 0000000..616688f --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/asset.dart
@@ -0,0 +1,323 @@ +// 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. + +import 'package:yaml/yaml.dart'; + +import '../utils/uri.dart'; +import '../utils/yaml.dart'; +import 'link_mode.dart'; +import 'target.dart'; + +abstract class AssetPath { + factory AssetPath(String pathType, Uri? uri) { + switch (pathType) { + case AssetAbsolutePath._pathTypeValue: + return AssetAbsolutePath(uri!); + case AssetRelativePath._pathTypeValue: + return AssetRelativePath(uri!); + case AssetSystemPath._pathTypeValue: + return AssetSystemPath(uri!); + case AssetInExecutable._pathTypeValue: + return AssetInExecutable(); + case AssetInProcess._pathTypeValue: + return AssetInProcess(); + } + throw FormatException('Unknown pathType: $pathType.'); + } + + factory AssetPath.fromYaml(YamlMap yamlMap) { + final pathType = as<String>(yamlMap[_pathTypeKey]); + final uriString = as<String?>(yamlMap[_uriKey]); + final uri = uriString != null ? Uri(path: uriString) : null; + return AssetPath(pathType, uri); + } + + Map<String, Object> toYaml(); + List<String> toDartConst(); + + static const _pathTypeKey = 'path_type'; + static const _uriKey = 'uri'; + + Future<bool> exists(); +} + +/// Asset at absolute path [uri]. +class AssetAbsolutePath implements AssetPath { + final Uri uri; + + AssetAbsolutePath(this.uri); + + static const _pathTypeValue = 'absolute'; + + @override + Map<String, Object> toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + AssetPath._uriKey: uri.toFilePath(), + }; + + @override + List<String> toDartConst() => [_pathTypeValue, uri.toFilePath()]; + + @override + int get hashCode => Object.hash(uri, 133711); + + @override + bool operator ==(Object other) { + if (other is! AssetAbsolutePath) { + return false; + } + return uri == other.uri; + } + + @override + Future<bool> exists() => uri.fileSystemEntity.exists(); +} + +/// Asset is avaliable on a relative path. +/// +/// If [LinkMode] of an [Asset] is [LinkMode.dynamic], +/// `Platform.script.resolve(uri)` will be used to load the asset at runtime. +class AssetRelativePath implements AssetPath { + final Uri uri; + + AssetRelativePath(this.uri); + + static const _pathTypeValue = 'relative'; + + @override + Map<String, Object> toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + AssetPath._uriKey: uri.toFilePath(), + }; + + @override + List<String> toDartConst() => [_pathTypeValue, uri.toFilePath()]; + + @override + int get hashCode => Object.hash(uri, 133717); + + @override + bool operator ==(Object other) { + if (other is! AssetRelativePath) { + return false; + } + return uri == other.uri; + } + + @override + Future<bool> exists() => uri.fileSystemEntity.exists(); +} + +/// Asset is avaliable on the system `PATH`. +/// +/// [uri] only contains a file name. +class AssetSystemPath implements AssetPath { + final Uri uri; + + AssetSystemPath(this.uri); + + static const _pathTypeValue = 'system'; + + @override + Map<String, Object> toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + AssetPath._uriKey: uri.toFilePath(), + }; + + @override + List<String> toDartConst() => [_pathTypeValue, uri.toFilePath()]; + + @override + int get hashCode => Object.hash(uri, 133723); + + @override + bool operator ==(Object other) { + if (other is! AssetSystemPath) { + return false; + } + return uri == other.uri; + } + + @override + Future<bool> exists() => Future.value(true); +} + +/// Asset is loaded in the process and symbols are available through +/// `DynamicLibrary.process()`. +class AssetInProcess implements AssetPath { + AssetInProcess._(); + + static final AssetInProcess _singleton = AssetInProcess._(); + + factory AssetInProcess() => _singleton; + + static const _pathTypeValue = 'process'; + + @override + Map<String, Object> toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + }; + + @override + List<String> toDartConst() => [_pathTypeValue]; + + @override + Future<bool> exists() => Future.value(true); +} + +/// Asset is embedded in executable and symbols are available through +/// `DynamicLibrary.executable()`. +class AssetInExecutable implements AssetPath { + AssetInExecutable._(); + + static final AssetInExecutable _singleton = AssetInExecutable._(); + + factory AssetInExecutable() => _singleton; + + static const _pathTypeValue = 'executable'; + + @override + Map<String, Object> toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + }; + + @override + List<String> toDartConst() => [_pathTypeValue]; + + @override + Future<bool> exists() => Future.value(true); +} + +class Asset { + final LinkMode linkMode; + final String id; + final Target target; + final AssetPath path; + + Asset({ + required this.id, + required this.linkMode, + required this.target, + required this.path, + }); + + factory Asset.fromYaml(YamlMap yamlMap) => Asset( + id: as<String>(yamlMap[_idKey]), + path: AssetPath.fromYaml(as<YamlMap>(yamlMap[_pathKey])), + target: Target.fromString(as<String>(yamlMap[_targetKey])), + linkMode: LinkMode.fromName(as<String>(yamlMap[_linkModeKey])), + ); + + static List<Asset> listFromYamlString(String yaml) { + final yamlObject = loadYaml(yaml); + if (yamlObject == null) { + return []; + } + return [ + for (final yamlElement in as<YamlList>(yamlObject)) + Asset.fromYaml(as<YamlMap>(yamlElement)), + ]; + } + + static List<Asset> listFromYamlList(YamlList yamlList) => [ + for (final yamlElement in yamlList) + Asset.fromYaml(as<YamlMap>(yamlElement)), + ]; + + Asset copyWith({ + LinkMode? linkMode, + String? id, + Target? target, + AssetPath? path, + }) => + Asset( + id: id ?? this.id, + linkMode: linkMode ?? this.linkMode, + target: target ?? this.target, + path: path ?? this.path, + ); + + @override + bool operator ==(Object other) { + if (other is! Asset) { + return false; + } + return other.id == id && + other.linkMode == linkMode && + other.target == target && + other.path == path; + } + + @override + int get hashCode => Object.hash(id, linkMode, target, path); + + Map<String, Object> toYaml() => { + _idKey: id, + _linkModeKey: linkMode.name, + _pathKey: path.toYaml(), + _targetKey: target.toString(), + }; + + Map<String, List<String>> toDartConst() => { + id: path.toDartConst(), + }; + + String toYamlString() => yamlEncode(toYaml()); + + static const _idKey = 'id'; + static const _linkModeKey = 'link_mode'; + static const _pathKey = 'path'; + static const _targetKey = 'target'; + + Future<bool> exists() => path.exists(); + + @override + String toString() => 'Asset(${toYaml()})'; +} + +extension AssetIterable on Iterable<Asset> { + List<Object> toYaml() => [for (final item in this) item.toYaml()]; + + String toYamlString() => yamlEncode(toYaml()); + + Iterable<Asset> whereLinkMode(LinkMode linkMode) => + where((e) => e.linkMode == linkMode); + + Map<Target, List<Asset>> get assetsPerTarget { + final result = <Target, List<Asset>>{}; + for (final asset in this) { + final assets = result[asset.target] ?? []; + assets.add(asset); + result[asset.target] = assets; + } + return result; + } + + Map<String, Map<String, List<String>>> toDartConst() => { + for (final entry in assetsPerTarget.entries) + entry.key.toString(): + _combineMaps(entry.value.map((e) => e.toDartConst()).toList()) + }; + + Map<Object, Object> toNativeAssetsFileEncoding() => { + 'format-version': [1, 0, 0], + 'native-assets': toDartConst(), + }; + + String toNativeAssetsFile() => yamlEncode(toNativeAssetsFileEncoding()); + + Future<bool> allExist() async { + final allResults = await Future.wait(map((e) => e.exists())); + final missing = allResults.contains(false); + return !missing; + } +} + +Map<X, Y> _combineMaps<X, Y>(Iterable<Map<X, Y>> maps) { + final result = <X, Y>{}; + for (final map in maps) { + result.addAll(map); + } + return result; +}
diff --git a/pkgs/native_assets_cli/lib/src/model/build_config.dart b/pkgs/native_assets_cli/lib/src/model/build_config.dart new file mode 100644 index 0000000..93e0d9d --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/build_config.dart
@@ -0,0 +1,658 @@ +// 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. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:cli_config/cli_config.dart'; +import 'package:collection/collection.dart'; +import 'package:crypto/crypto.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import '../utils/map.dart'; +import '../utils/yaml.dart'; +import 'build_mode.dart'; +import 'ios_sdk.dart'; +import 'link_mode_preference.dart'; +import 'metadata.dart'; +import 'target.dart'; + +class BuildConfig { + /// The folder in which all output and intermediate artifacts should be + /// placed. + Uri get outDir => _outDir; + late final Uri _outDir; + + /// The name of the package the native assets are built for. + String get packageName => _packageName; + late final String _packageName; + + /// The root of the package the native assets are built for. + /// + /// Often a package's native assets are built because a package is a + /// dependency of another. For this it is convenient to know the packageRoot. + Uri get packageRoot => _packageRoot; + late final Uri _packageRoot; + + /// The target being compiled for. + /// + /// Not available in [dryRun]. + late final Target target = + Target.fromArchitectureAndOs(targetArchitecture, targetOs); + + /// The architecture being compiled for. + /// + /// Not available in [dryRun]. + Architecture get targetArchitecture { + _ensureNotDryRun(); + return _targetArchitecture; + } + + late final Architecture _targetArchitecture; + + /// The operating system being compiled for. + OS get targetOs => _targetOs; + late final OS _targetOs; + + /// When compiling for iOS, whether to target device or simulator. + /// + /// Required when [targetOs] equals [OS.iOS]. + /// + /// Not available in [dryRun].s + IOSSdk? get targetIOSSdk { + _ensureNotDryRun(); + return _targetIOSSdk; + } + + late final IOSSdk? _targetIOSSdk; + + /// When compiling for Android, the minimum Android SDK API version to that + /// the compiled code will be compatible with. + /// + /// Required when [targetOs] equals [OS.android]. + /// + /// Not available in [dryRun]. + /// + /// For more information about the Android API version, refer to + /// [`minSdkVersion`](https://developer.android.com/ndk/guides/sdk-versions#minsdkversion) + /// in the Android documentation. + int? get targetAndroidNdkApi { + _ensureNotDryRun(); + return _targetAndroidNdkApi; + } + + late final int? _targetAndroidNdkApi; + + /// Preferred linkMode method for library. + LinkModePreference get linkModePreference => _linkModePreference; + late final LinkModePreference _linkModePreference; + + /// Metadata from direct dependencies. + /// + /// The key in the map is the package name of the dependency. + /// + /// The key in the nested map is the key for the metadata from the dependency. + /// + /// Not available in [dryRun]. + Map<String, Metadata>? get dependencyMetadata { + _ensureNotDryRun(); + return _dependencyMetadata; + } + + late final Map<String, Metadata>? _dependencyMetadata; + + /// The configuration for invoking the C compiler. + /// + /// Not available in [dryRun]. + CCompilerConfig get cCompiler { + _ensureNotDryRun(); + return _cCompiler; + } + + late final CCompilerConfig _cCompiler; + + /// Don't run the build, only report the native assets produced. + bool get dryRun => _dryRun ?? false; + late final bool? _dryRun; + + /// The build mode that the code should be compiled in. + /// + /// Not available in [dryRun]. + BuildMode get buildMode { + _ensureNotDryRun(); + return _buildMode; + } + + late final BuildMode _buildMode; + + /// The underlying config. + /// + /// Can be used for easier access to values on [dependencyMetadata]. + Config get config => _config; + late final Config _config; + + factory BuildConfig({ + required Uri outDir, + required String packageName, + required Uri packageRoot, + required BuildMode buildMode, + required Architecture targetArchitecture, + required OS targetOs, + IOSSdk? targetIOSSdk, + int? targetAndroidNdkApi, + CCompilerConfig? cCompiler, + required LinkModePreference linkModePreference, + Map<String, Metadata>? dependencyMetadata, + }) { + final nonValidated = BuildConfig._() + .._outDir = outDir + .._packageName = packageName + .._packageRoot = packageRoot + .._buildMode = buildMode + .._targetArchitecture = targetArchitecture + .._targetOs = targetOs + .._targetIOSSdk = targetIOSSdk + .._targetAndroidNdkApi = targetAndroidNdkApi + .._cCompiler = cCompiler ?? CCompilerConfig() + .._linkModePreference = linkModePreference + .._dependencyMetadata = dependencyMetadata + .._dryRun = false; + final parsedConfigFile = nonValidated.toYaml(); + final config = Config(fileParsed: parsedConfigFile); + return BuildConfig.fromConfig(config); + } + + factory BuildConfig.dryRun({ + required Uri outDir, + required String packageName, + required Uri packageRoot, + required OS targetOs, + required LinkModePreference linkModePreference, + }) { + final nonValidated = BuildConfig._() + .._outDir = outDir + .._packageName = packageName + .._packageRoot = packageRoot + .._targetOs = targetOs + .._linkModePreference = linkModePreference + .._cCompiler = CCompilerConfig() + .._dryRun = true; + final parsedConfigFile = nonValidated.toYaml(); + final config = Config(fileParsed: parsedConfigFile); + return BuildConfig.fromConfig(config); + } + + /// Constructs a checksum for a [BuildConfig] based on the fields + /// of a buildconfig that influence the build. + /// + /// This can be used for an [outDir], but should not be used for dry-runs. + /// + /// In particular, it only takes the package name from [packageRoot], + /// so that the hash is equal across checkouts and ignores [outDir] itself. + static String checksum({ + required String packageName, + required Uri packageRoot, + required Architecture targetArchitecture, + required OS targetOs, + required BuildMode buildMode, + IOSSdk? targetIOSSdk, + int? targetAndroidNdkApi, + CCompilerConfig? cCompiler, + required LinkModePreference linkModePreference, + Map<String, Metadata>? dependencyMetadata, + }) { + final input = [ + packageName, + targetArchitecture.toString(), + targetOs.toString(), + targetIOSSdk.toString(), + targetAndroidNdkApi.toString(), + buildMode.toString(), + linkModePreference.toString(), + cCompiler?.ar.toString(), + cCompiler?.cc.toString(), + cCompiler?.envScript.toString(), + cCompiler?.envScriptArgs.toString(), + cCompiler?.ld.toString(), + if (dependencyMetadata != null) + for (final entry in dependencyMetadata.entries) ...[ + entry.key, + json.encode(entry.value.toYaml()), + ] + ].join('###'); + final sha256String = sha256.convert(utf8.encode(input)).toString(); + // 256 bit hashes lead to 64 hex character strings. + // To avoid overflowing file paths limits, only use 32. + // Using 16 hex characters would also be unlikely to have collisions. + const nameLength = 32; + return sha256String.substring(0, nameLength); + } + + BuildConfig._(); + + /// The version of [BuildConfig]. + /// + /// This class is used in the protocol between the Dart and Flutter SDKs + /// and packages through `build.dart` invocations. + /// + /// If we ever were to make breaking changes, it would be useful to give + /// proper error messages rather than just fail to parse the YAML + /// representation in the protocol. + static Version version = Version(1, 0, 0); + + factory BuildConfig.fromConfig(Config config) { + final result = BuildConfig._().._cCompiler = CCompilerConfig._(); + final configExceptions = <Object>[]; + for (final f in result._readFieldsFromConfig()) { + try { + f(config); + } on FormatException catch (e, st) { + configExceptions.add(e); + configExceptions.add(st); + } + } + + if (configExceptions.isNotEmpty) { + throw FormatException('Configuration is not in the right format. ' + 'FormatExceptions: $configExceptions'); + } + + return result; + } + + /// Constructs a config by parsing CLI arguments and loading the config file. + /// + /// The [args] must be commandline arguments. + /// + /// If provided, [environment] must be a map containing environment variables. + /// If not provided, [environment] defaults to [Platform.environment]. + /// + /// If provided, [workingDirectory] is used to resolves paths inside + /// [environment]. + /// If not provided, [workingDirectory] defaults to [Directory.current]. + /// + /// This async constructor is intended to be used directly in CLI files. + static Future<BuildConfig> fromArgs( + List<String> args, { + Map<String, String>? environment, + Uri? workingDirectory, + }) async { + final config = await Config.fromArgs( + args: args, + environment: environment, + workingDirectory: workingDirectory, + ); + return BuildConfig.fromConfig(config); + } + + static const outDirConfigKey = 'out_dir'; + static const packageNameConfigKey = 'package_name'; + static const packageRootConfigKey = 'package_root'; + static const dependencyMetadataConfigKey = 'dependency_metadata'; + static const _versionKey = 'version'; + static const targetAndroidNdkApiConfigKey = 'target_android_ndk_api'; + static const dryRunConfigKey = 'dry_run'; + + List<void Function(Config)> _readFieldsFromConfig() { + var osSet = false; + var ccSet = false; + return [ + (config) { + final configVersion = Version.parse(config.string('version')); + if (configVersion.major > version.major) { + throw FormatException( + 'The config version $configVersion is newer than this ' + 'package:native_assets_cli config version $version, ' + 'please update native_assets_cli.', + ); + } + if (configVersion.major < version.major) { + throw FormatException( + 'The config version $configVersion is newer than this ' + 'package:native_assets_cli config version $version, ' + 'please update the Dart or Flutter SDK.', + ); + } + }, + (config) => _config = config, + (config) => _dryRun = config.optionalBool(dryRunConfigKey), + (config) => _outDir = config.path(outDirConfigKey, mustExist: true), + (config) => _packageName = config.string(packageNameConfigKey), + (config) => + _packageRoot = config.path(packageRootConfigKey, mustExist: true), + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<String>(BuildMode.configKey); + } else { + _buildMode = BuildMode.fromString( + config.string( + BuildMode.configKey, + validValues: BuildMode.values.map((e) => '$e'), + ), + ); + } + }, + (config) { + _targetOs = OS.fromString( + config.string( + OS.configKey, + validValues: OS.values.map((e) => '$e'), + ), + ); + osSet = true; + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<String>(Architecture.configKey); + } else { + final validArchitectures = [ + if (!osSet) + ...Architecture.values + else + for (final target in Target.values) + if (target.os == _targetOs) target.architecture + ]; + _targetArchitecture = Architecture.fromString( + config.string( + Architecture.configKey, + validValues: validArchitectures.map((e) => '$e'), + ), + ); + } + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<String>(IOSSdk.configKey); + } else { + _targetIOSSdk = (osSet && _targetOs == OS.iOS) + ? IOSSdk.fromString( + config.string( + IOSSdk.configKey, + validValues: IOSSdk.values.map((e) => '$e'), + ), + ) + : null; + } + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<int>(targetAndroidNdkApiConfigKey); + } else { + _targetAndroidNdkApi = (osSet && _targetOs == OS.android) + ? config.int(targetAndroidNdkApiConfigKey) + : null; + } + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<int>(CCompilerConfig.arConfigKeyFull); + } else { + cCompiler._ar = config.optionalPath( + CCompilerConfig.arConfigKeyFull, + mustExist: true, + ); + } + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull); + } else { + cCompiler._cc = config.optionalPath( + CCompilerConfig.ccConfigKeyFull, + mustExist: true, + ); + ccSet = true; + } + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull); + } else { + cCompiler._ld = config.optionalPath( + CCompilerConfig.ldConfigKeyFull, + mustExist: true, + ); + } + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull); + } else { + cCompiler._envScript = (ccSet && + cCompiler.cc != null && + cCompiler.cc!.toFilePath().endsWith('cl.exe')) + ? config.path(CCompilerConfig.envScriptConfigKeyFull, + mustExist: true) + : null; + } + }, + (config) { + if (dryRun) { + _throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull); + } else { + cCompiler._envScriptArgs = config.optionalStringList( + CCompilerConfig.envScriptArgsConfigKeyFull, + splitEnvironmentPattern: ' ', + ); + } + }, + (config) { + _linkModePreference = LinkModePreference.fromString( + config.string( + LinkModePreference.configKey, + validValues: LinkModePreference.values.map((e) => '$e'), + ), + ); + }, + (config) { + _dependencyMetadata = _readDependencyMetadataFromConfig(config); + }, + ]; + } + + Map<String, Metadata>? _readDependencyMetadataFromConfig(Config config) { + final fileValue = + config.valueOf<Map<Object?, Object?>?>(dependencyMetadataConfigKey); + if (fileValue == null) { + return null; + } + final result = <String, Metadata>{}; + for (final entry in fileValue.entries) { + final packageName = as<String>(entry.key); + final defines = entry.value; + if (defines is! Map) { + throw FormatException("Unexpected value '$defines' for key " + "'$dependencyMetadataConfigKey.$packageName' in config file. " + 'Expected a Map.'); + } + final packageResult = <String, Object>{}; + for (final entry2 in defines.entries) { + final key = as<String>(entry2.key); + final value = as<Object>(entry2.value); + packageResult[key] = value; + } + result[packageName] = Metadata(packageResult.sortOnKey()); + } + return result.sortOnKey(); + } + + Map<String, Object> toYaml() { + late Map<String, Object> cCompilerYaml; + if (!dryRun) { + cCompilerYaml = _cCompiler.toYaml(); + } + + return { + outDirConfigKey: _outDir.toFilePath(), + packageNameConfigKey: _packageName, + packageRootConfigKey: _packageRoot.toFilePath(), + OS.configKey: _targetOs.toString(), + LinkModePreference.configKey: _linkModePreference.toString(), + _versionKey: version.toString(), + if (dryRun) dryRunConfigKey: dryRun, + if (!dryRun) ...{ + BuildMode.configKey: _buildMode.toString(), + Architecture.configKey: _targetArchitecture.toString(), + if (_targetIOSSdk != null) IOSSdk.configKey: _targetIOSSdk.toString(), + if (_targetAndroidNdkApi != null) + targetAndroidNdkApiConfigKey: _targetAndroidNdkApi!, + if (cCompilerYaml.isNotEmpty) CCompilerConfig.configKey: cCompilerYaml, + if (_dependencyMetadata != null) + dependencyMetadataConfigKey: { + for (final entry in _dependencyMetadata!.entries) + entry.key: entry.value.toYaml(), + }, + }, + }.sortOnKey(); + } + + String toYamlString() => yamlEncode(toYaml()); + + @override + bool operator ==(Object other) { + if (other is! BuildConfig) { + return false; + } + if (other.outDir != outDir) return false; + if (other.packageName != packageName) return false; + if (other.packageRoot != packageRoot) return false; + if (other.dryRun != dryRun) return false; + if (other.targetOs != targetOs) return false; + if (other.linkModePreference != linkModePreference) return false; + if (!dryRun) { + if (other.buildMode != buildMode) return false; + if (other.targetArchitecture != targetArchitecture) return false; + if (other.targetIOSSdk != targetIOSSdk) return false; + if (other.targetAndroidNdkApi != targetAndroidNdkApi) return false; + if (other.cCompiler != cCompiler) return false; + if (!const DeepCollectionEquality() + .equals(other.dependencyMetadata, _dependencyMetadata)) return false; + } + return true; + } + + @override + int get hashCode => Object.hashAll([ + outDir, + packageName, + packageRoot, + targetOs, + linkModePreference, + dryRun, + if (!dryRun) ...[ + buildMode, + const DeepCollectionEquality().hash(dependencyMetadata), + targetArchitecture, + targetIOSSdk, + targetAndroidNdkApi, + cCompiler, + ], + ]); + + @override + String toString() => 'BuildConfig(${toYaml()})'; + + void _ensureNotDryRun() { + if (dryRun) { + throw StateError('''This field is not available in dry runs. +In Flutter projects, native builds are generated per OS which target multiple +architectures, build modes, etc. Therefore, the list of native assets produced +can _only_ depend on OS.'''); + } + } + + void _throwIfNotNullInDryRun<T>(String key) { + final object = config.valueOf<T?>(key); + if (object != null) { + throw const FormatException('''This field is not available in dry runs. +In Flutter projects, native builds are generated per OS which target multiple +architectures, build modes, etc. Therefore, the list of native assets produced +can _only_ depend on OS.'''); + } + } +} + +class CCompilerConfig { + /// Path to a C compiler. + Uri? get cc => _cc; + late final Uri? _cc; + + /// Path to a native linker. + Uri? get ld => _ld; + late final Uri? _ld; + + /// Path to a native archiver. + Uri? get ar => _ar; + late final Uri? _ar; + + /// Path to script that sets environment variables for [cc], [ld], and [ar]. + Uri? get envScript => _envScript; + late final Uri? _envScript; + + /// Arguments for [envScript]. + List<String>? get envScriptArgs => _envScriptArgs; + late final List<String>? _envScriptArgs; + + factory CCompilerConfig({ + Uri? ar, + Uri? cc, + Uri? ld, + Uri? envScript, + List<String>? envScriptArgs, + }) => + CCompilerConfig._() + .._ar = ar + .._cc = cc + .._ld = ld + .._envScript = envScript + .._envScriptArgs = envScriptArgs; + + CCompilerConfig._(); + + static const configKey = 'c_compiler'; + static const arConfigKey = 'ar'; + static const arConfigKeyFull = '$configKey.$arConfigKey'; + static const ccConfigKey = 'cc'; + static const ccConfigKeyFull = '$configKey.$ccConfigKey'; + static const ldConfigKey = 'ld'; + static const ldConfigKeyFull = '$configKey.$ldConfigKey'; + static const envScriptConfigKey = 'env_script'; + static const envScriptConfigKeyFull = '$configKey.$envScriptConfigKey'; + static const envScriptArgsConfigKey = 'env_script_arguments'; + static const envScriptArgsConfigKeyFull = + '$configKey.$envScriptArgsConfigKey'; + + Map<String, Object> toYaml() => { + if (_ar != null) arConfigKey: _ar!.toFilePath(), + if (_cc != null) ccConfigKey: _cc!.toFilePath(), + if (_ld != null) ldConfigKey: _ld!.toFilePath(), + if (_envScript != null) envScriptConfigKey: _envScript!.toFilePath(), + if (_envScriptArgs != null) envScriptArgsConfigKey: _envScriptArgs!, + }.sortOnKey(); + + @override + bool operator ==(Object other) { + if (other is! CCompilerConfig) { + return false; + } + if (other.ar != ar) return false; + if (other.cc != cc) return false; + if (other.ld != ld) return false; + if (other.envScript != envScript) return false; + if (!const ListEquality<String>() + .equals(other.envScriptArgs, envScriptArgs)) { + return false; + } + return true; + } + + @override + int get hashCode => Object.hash( + _ar, + _cc, + _ld, + _envScript, + const ListEquality<String>().hash(envScriptArgs), + ); +}
diff --git a/pkgs/native_assets_cli/lib/src/model/build_mode.dart b/pkgs/native_assets_cli/lib/src/model/build_mode.dart new file mode 100644 index 0000000..8b8cd3c --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/build_mode.dart
@@ -0,0 +1,26 @@ +// 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. + +class BuildMode { + final String name; + + const BuildMode._(this.name); + + static const debug = BuildMode._('debug'); + static const release = BuildMode._('release'); + + static const values = [ + debug, + release, + ]; + + factory BuildMode.fromString(String target) => + values.firstWhere((e) => e.name == target); + + /// The `package:config` key preferably used. + static const String configKey = 'build_mode'; + + @override + String toString() => name; +}
diff --git a/pkgs/native_assets_cli/lib/src/model/build_output.dart b/pkgs/native_assets_cli/lib/src/model/build_output.dart new file mode 100644 index 0000000..6f88407 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/build_output.dart
@@ -0,0 +1,138 @@ +// 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. + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml/yaml.dart'; + +import '../utils/datetime.dart'; +import '../utils/file.dart'; +import '../utils/map.dart'; +import '../utils/yaml.dart'; +import 'asset.dart'; +import 'dependencies.dart'; +import 'metadata.dart'; + +class BuildOutput { + /// Time the build this output belongs to started. + /// + /// Rounded down to whole seconds, because [File.lastModified] is rounded + /// to whole seconds and caching logic compares these timestamps. + final DateTime timestamp; + final List<Asset> assets; + final Dependencies dependencies; + final Metadata metadata; + + BuildOutput({ + DateTime? timestamp, + List<Asset>? assets, + Dependencies? dependencies, + Metadata? metadata, + }) : timestamp = (timestamp ?? DateTime.now()).roundDownToSeconds(), + assets = assets ?? [], + // ignore: prefer_const_constructors + dependencies = dependencies ?? Dependencies([]), + // ignore: prefer_const_constructors + metadata = metadata ?? Metadata({}); + + static const _assetsKey = 'assets'; + static const _dependenciesKey = 'dependencies'; + static const _metadataKey = 'metadata'; + static const _timestampKey = 'timestamp'; + static const _versionKey = 'version'; + + factory BuildOutput.fromYamlString(String yaml) { + final yamlObject = loadYaml(yaml); + return BuildOutput.fromYaml(as<YamlMap>(yamlObject)); + } + + factory BuildOutput.fromYaml(YamlMap yamlMap) { + final outputVersion = Version.parse(as<String>(yamlMap['version'])); + if (outputVersion.major > version.major) { + throw FormatException( + 'The output version $outputVersion is newer than the ' + 'package:native_assets_cli config version $version in Dart or Flutter, ' + 'please update the Dart or Flutter SDK.', + ); + } + if (outputVersion.major < version.major) { + throw FormatException( + 'The output version $outputVersion is newer than this ' + 'package:native_assets_cli config version $version in Dart or Flutter, ' + 'please update native_assets_cli.', + ); + } + + return BuildOutput( + timestamp: DateTime.parse(as<String>(yamlMap[_timestampKey])), + assets: Asset.listFromYamlList(as<YamlList>(yamlMap[_assetsKey])), + dependencies: + Dependencies.fromYaml(as<YamlList?>(yamlMap[_dependenciesKey])), + metadata: Metadata.fromYaml(as<YamlMap?>(yamlMap[_metadataKey])), + ); + } + + Map<String, Object> toYaml() => { + _timestampKey: timestamp.toString(), + _assetsKey: assets.toYaml(), + _dependenciesKey: dependencies.toYaml(), + _metadataKey: metadata.toYaml(), + _versionKey: version.toString(), + }..sortOnKey(); + + String toYamlString() => yamlEncode(toYaml()); + + /// The version of [BuildOutput]. + /// + /// This class is used in the protocol between the Dart and Flutter SDKs + /// and packages through `build.dart` invocations. + /// + /// If we ever were to make breaking changes, it would be useful to give + /// proper error messages rather than just fail to parse the YAML + /// representation in the protocol. + static Version version = Version(1, 0, 0); + + static const fileName = 'build_output.yaml'; + + /// Writes the YAML file from [outDir]/[fileName]. + static Future<BuildOutput?> readFromFile({required Uri outDir}) async { + final buildOutputUri = outDir.resolve(fileName); + final buildOutputFile = File.fromUri(buildOutputUri); + if (!await buildOutputFile.exists()) { + return null; + } + return BuildOutput.fromYamlString(await buildOutputFile.readAsString()); + } + + /// Writes the [toYamlString] to [outDir]/[fileName]. + Future<void> writeToFile({required Uri outDir}) async { + final buildOutputUri = outDir.resolve(fileName); + await File.fromUri(buildOutputUri) + .writeAsStringCreateDirectory(toYamlString()); + } + + @override + String toString() => toYamlString(); + + @override + bool operator ==(Object other) { + if (other is! BuildOutput) { + return false; + } + return other.timestamp == timestamp && + const ListEquality<Asset>().equals(other.assets, assets) && + other.dependencies == dependencies && + other.metadata == metadata; + } + + @override + int get hashCode => Object.hash( + timestamp.hashCode, + const ListEquality<Asset>().hash(assets), + dependencies, + metadata, + ); +}
diff --git a/pkgs/native_assets_cli/lib/src/model/dependencies.dart b/pkgs/native_assets_cli/lib/src/model/dependencies.dart new file mode 100644 index 0000000..9ecc2cb --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/dependencies.dart
@@ -0,0 +1,55 @@ +// 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. + +import 'package:collection/collection.dart'; +import 'package:yaml/yaml.dart'; + +import '../utils/file.dart'; +import '../utils/uri.dart'; +import '../utils/yaml.dart'; + +class Dependencies { + /// The dependencies a build relied on. + final List<Uri> dependencies; + + const Dependencies(this.dependencies); + + factory Dependencies.fromYamlString(String yamlString) { + final yaml = loadYaml(yamlString); + if (yaml is YamlList) { + return Dependencies.fromYaml(yaml); + } + // ignore: prefer_const_constructors + return Dependencies([]); + } + + factory Dependencies.fromYaml(YamlList? yamlList) => Dependencies([ + if (yamlList != null) + for (final dependency in yamlList) + fileSystemPathToUri(as<String>(dependency)), + ]); + + List<String> toYaml() => [ + for (final dependency in dependencies) dependency.toFilePath(), + ]; + + String toYamlString() => yamlEncode(toYaml()); + + @override + String toString() => toYamlString(); + + Future<DateTime> lastModified() => + dependencies.map((u) => u.fileSystemEntity).lastModified(); + + @override + bool operator ==(Object other) { + if (other is! Dependencies) { + return false; + } + return const ListEquality<Uri>().equals(other.dependencies, dependencies); + } + + @override + int get hashCode => const ListEquality<Uri>().hash(dependencies); +}
diff --git a/pkgs/native_assets_cli/lib/src/model/ios_sdk.dart b/pkgs/native_assets_cli/lib/src/model/ios_sdk.dart new file mode 100644 index 0000000..7d3deeb --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/ios_sdk.dart
@@ -0,0 +1,29 @@ +// 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. + +/// For an iOS target, a build is either done for the device or the simulator. +/// +/// Only fat binaries or xcframeworks can contain both targets. +class IOSSdk { + final String xcodebuildSdk; + + const IOSSdk._(this.xcodebuildSdk); + + static const iPhoneOs = IOSSdk._('iphoneos'); + static const iPhoneSimulator = IOSSdk._('iphonesimulator'); + + static const values = [ + iPhoneOs, + iPhoneSimulator, + ]; + + factory IOSSdk.fromString(String target) => + values.firstWhere((e) => e.xcodebuildSdk == target); + + /// The `package:config` key preferably used. + static const String configKey = 'target_ios_sdk'; + + @override + String toString() => xcodebuildSdk; +}
diff --git a/pkgs/native_assets_cli/lib/src/model/link_mode.dart b/pkgs/native_assets_cli/lib/src/model/link_mode.dart new file mode 100644 index 0000000..a7f4d3d --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/link_mode.dart
@@ -0,0 +1,24 @@ +// 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. + +class LinkMode { + final String name; + + const LinkMode._(this.name); + + static const LinkMode dynamic = LinkMode._('dynamic'); + static const LinkMode static = LinkMode._('static'); + + /// Known values for [LinkMode]. + static const List<LinkMode> values = [ + dynamic, + static, + ]; + + factory LinkMode.fromName(String name) => + values.where((element) => element.name == name).first; + + @override + String toString() => name; +}
diff --git a/pkgs/native_assets_cli/lib/src/model/link_mode_preference.dart b/pkgs/native_assets_cli/lib/src/model/link_mode_preference.dart new file mode 100644 index 0000000..89a72ce --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/link_mode_preference.dart
@@ -0,0 +1,72 @@ +// 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. + +import 'link_mode.dart'; + +class LinkModePreference { + final String name; + final String description; + final LinkMode preferredLinkMode; + final List<LinkMode> potentialLinkMode; + + const LinkModePreference( + this.name, + this.description, { + required this.preferredLinkMode, + required this.potentialLinkMode, + }); + + factory LinkModePreference.fromString(String name) => + values.where((element) => element.name == name).first; + + static const dynamic = LinkModePreference( + 'dynamic', + '''Provide native assets as dynamic libraries. +Fails if not all native assets can only be provided as static library. +Required to run Dart in JIT mode.''', + preferredLinkMode: LinkMode.dynamic, + potentialLinkMode: [LinkMode.dynamic], + ); + + static const static = LinkModePreference( + 'static', + '''Provide native assets as static libraries. +Fails if not all native assets can only be provided as dynamic library. +Required for potential link-time tree-shaking of native code. +Therefore, preferred to in Dart AOT mode.''', + preferredLinkMode: LinkMode.static, + potentialLinkMode: [LinkMode.static], + ); + + static const preferDynamic = LinkModePreference( + 'prefer-dynamic', + '''Provide native assets as dynamic libraries, if possible. +Otherwise, build native assets as static libraries.''', + preferredLinkMode: LinkMode.dynamic, + potentialLinkMode: LinkMode.values, + ); + + static const preferStatic = LinkModePreference( + 'prefer-static', + '''Provide native assets as static libraries, if possible. +Otherwise, build native assets as dynamic libraries. +Preferred for AOT compilation, if there are any native assets which can only be +provided as dynamic libraries.''', + preferredLinkMode: LinkMode.static, + potentialLinkMode: LinkMode.values, + ); + + static const values = [ + dynamic, + static, + preferDynamic, + preferStatic, + ]; + + /// The `package:config` key preferably used. + static const String configKey = 'link_mode_preference'; + + @override + String toString() => name; +}
diff --git a/pkgs/native_assets_cli/lib/src/model/metadata.dart b/pkgs/native_assets_cli/lib/src/model/metadata.dart new file mode 100644 index 0000000..34949b1 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/metadata.dart
@@ -0,0 +1,41 @@ +// 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. + +import 'package:collection/collection.dart'; +import 'package:yaml/yaml.dart'; + +import '../utils/map.dart'; +import '../utils/yaml.dart'; + +class Metadata { + final Map<String, Object> metadata; + + const Metadata(this.metadata); + + factory Metadata.fromYaml(YamlMap? yamlMap) => + Metadata(yamlMap?.formatCast<String, Object>() ?? {}); + + factory Metadata.fromYamlString(String yaml) { + final yamlObject = as<YamlMap>(loadYaml(yaml)); + return Metadata.fromYaml(yamlObject); + } + + Map<String, Object> toYaml() => metadata..sortOnKey(); + + String toYamlString() => yamlEncode(toYaml()); + + @override + bool operator ==(Object other) { + if (other is! Metadata) { + return false; + } + return const DeepCollectionEquality().equals(other.metadata, metadata); + } + + @override + int get hashCode => const DeepCollectionEquality().hash(metadata); + + @override + String toString() => 'Metadata(${toYaml()})'; +}
diff --git a/pkgs/native_assets_cli/lib/src/model/target.dart b/pkgs/native_assets_cli/lib/src/model/target.dart new file mode 100644 index 0000000..db61c62 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/target.dart
@@ -0,0 +1,372 @@ +// 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. + +import 'dart:ffi' show Abi; +import 'dart:io'; + +import 'link_mode.dart'; + +/// The hardware architectures the Dart VM runs on. +class Architecture { + /// This architecture as used in [Platform.version]. + final String dartPlatform; + + const Architecture._(this.dartPlatform); + + factory Architecture.fromAbi(Abi abi) => _abiToArch[abi]!; + + static const Architecture arm = Architecture._('arm'); + static const Architecture arm64 = Architecture._('arm64'); + static const Architecture ia32 = Architecture._('ia32'); + static const Architecture riscv32 = Architecture._('riscv32'); + static const Architecture riscv64 = Architecture._('riscv64'); + static const Architecture x64 = Architecture._('x64'); + + /// Known values for [Architecture]. + static const List<Architecture> values = [ + arm, + arm64, + ia32, + riscv32, + riscv64, + x64, + ]; + + static const _abiToArch = { + Abi.androidArm: Architecture.arm, + Abi.androidArm64: Architecture.arm64, + Abi.androidIA32: Architecture.ia32, + Abi.androidX64: Architecture.x64, + Abi.androidRiscv64: Architecture.riscv64, + Abi.fuchsiaArm64: Architecture.arm64, + Abi.fuchsiaX64: Architecture.x64, + Abi.iosArm: Architecture.arm, + Abi.iosArm64: Architecture.arm64, + Abi.iosX64: Architecture.x64, + Abi.linuxArm: Architecture.arm, + Abi.linuxArm64: Architecture.arm64, + Abi.linuxIA32: Architecture.ia32, + Abi.linuxRiscv32: Architecture.riscv32, + Abi.linuxRiscv64: Architecture.riscv64, + Abi.linuxX64: Architecture.x64, + Abi.macosArm64: Architecture.arm64, + Abi.macosX64: Architecture.x64, + Abi.windowsArm64: Architecture.arm64, + Abi.windowsIA32: Architecture.ia32, + Abi.windowsX64: Architecture.x64, + }; + + /// The `package:config` key preferably used. + static const String configKey = 'target_architecture'; + + @override + String toString() => dartPlatform; + + /// Mapping from strings as used in [Architecture.toString] to + /// [Architecture]s. + static final Map<String, Architecture> _stringToArchitecture = + Map.fromEntries(Architecture.values.map( + (architecture) => MapEntry(architecture.toString(), architecture))); + + factory Architecture.fromString(String target) => + _stringToArchitecture[target]!; + + /// The current [Architecture]. + /// + /// Read from the [Platform.version] string. + static final Architecture current = Target.current.architecture; +} + +/// The operating systems the Dart VM runs on. +class OS { + /// This OS as used in [Platform.version] + final String dartPlatform; + + const OS._(this.dartPlatform); + + factory OS.fromAbi(Abi abi) => _abiToOS[abi]!; + + static const OS android = OS._('android'); + static const OS fuchsia = OS._('fuchsia'); + static const OS iOS = OS._('ios'); + static const OS linux = OS._('linux'); + static const OS macOS = OS._('macos'); + static const OS windows = OS._('windows'); + + /// Known values for [OS]. + static const List<OS> values = [ + android, + fuchsia, + iOS, + linux, + macOS, + windows, + ]; + + static const _abiToOS = { + Abi.androidArm: OS.android, + Abi.androidArm64: OS.android, + Abi.androidIA32: OS.android, + Abi.androidX64: OS.android, + Abi.androidRiscv64: OS.android, + Abi.fuchsiaArm64: OS.fuchsia, + Abi.fuchsiaX64: OS.fuchsia, + Abi.iosArm: OS.iOS, + Abi.iosArm64: OS.iOS, + Abi.iosX64: OS.iOS, + Abi.linuxArm: OS.linux, + Abi.linuxArm64: OS.linux, + Abi.linuxIA32: OS.linux, + Abi.linuxRiscv32: OS.linux, + Abi.linuxRiscv64: OS.linux, + Abi.linuxX64: OS.linux, + Abi.macosArm64: OS.macOS, + Abi.macosX64: OS.macOS, + Abi.windowsArm64: OS.windows, + Abi.windowsIA32: OS.windows, + Abi.windowsX64: OS.windows, + }; + + /// Typical cross compilation between OSes. + static const _osCrossCompilationDefault = { + OS.macOS: [OS.macOS, OS.iOS, OS.android], + OS.linux: [OS.linux, OS.android], + OS.windows: [OS.windows, OS.android], + }; + + /// The default dynamic library file name on this [OS]. + String dylibFileName(String name) { + final prefix = _dylibPrefix[this]!; + final extension = _dylibExtension[this]!; + return '$prefix$name.$extension'; + } + + /// The default static library file name on this [OS]. + String staticlibFileName(String name) { + final prefix = _staticlibPrefix[this]!; + final extension = _staticlibExtension[this]!; + return '$prefix$name.$extension'; + } + + String libraryFileName(String name, LinkMode linkMode) { + if (linkMode == LinkMode.dynamic) { + return dylibFileName(name); + } + assert(linkMode == LinkMode.static); + return staticlibFileName(name); + } + + /// The default executable file name on this [OS]. + String executableFileName(String name) { + final extension = _executableExtension[this]!; + final dot = extension.isNotEmpty ? '.' : ''; + return '$name$dot$extension'; + } + + /// The default name prefix for dynamic libraries per [OS]. + static const _dylibPrefix = { + OS.android: 'lib', + OS.fuchsia: 'lib', + OS.iOS: 'lib', + OS.linux: 'lib', + OS.macOS: 'lib', + OS.windows: '', + }; + + /// The default extension for dynamic libraries per [OS]. + static const _dylibExtension = { + OS.android: 'so', + OS.fuchsia: 'so', + OS.iOS: 'dylib', + OS.linux: 'so', + OS.macOS: 'dylib', + OS.windows: 'dll', + }; + + /// The default name prefix for static libraries per [OS]. + static const _staticlibPrefix = _dylibPrefix; + + /// The default extension for static libraries per [OS]. + static const _staticlibExtension = { + OS.android: 'a', + OS.fuchsia: 'a', + OS.iOS: 'a', + OS.linux: 'a', + OS.macOS: 'a', + OS.windows: 'lib', + }; + + /// The default extension for executables per [OS]. + static const _executableExtension = { + OS.android: '', + OS.fuchsia: '', + OS.iOS: '', + OS.linux: '', + OS.macOS: '', + OS.windows: 'exe', + }; + + /// The `package:config` key preferably used. + static const String configKey = 'target_os'; + + @override + String toString() => dartPlatform; + + /// Mapping from strings as used in [OS.toString] to + /// [OS]s. + static final Map<String, OS> _stringToOS = + Map.fromEntries(OS.values.map((os) => MapEntry(os.toString(), os))); + + factory OS.fromString(String target) => _stringToOS[target]!; + + /// The current [OS]. + /// + /// Read from the [Platform.version] string. + static final OS current = Target.current.os; +} + +/// Application binary interface. +/// +/// The Dart VM can run on a variety of [Target]s, see [Target.values]. +class Target implements Comparable<Target> { + final Abi abi; + + const Target._(this.abi); + + factory Target.fromString(String target) => _stringToTarget[target]!; + + /// The [Target] corresponding the substring of [Platform.version] + /// describing the [Target]. + /// + /// The [Platform.version] strings are formatted as follows: + /// `<version> (<date>) on "<Target>"`. + factory Target.fromDartPlatform(String versionStringFull) { + final split = versionStringFull.split('"'); + if (split.length < 2) { + throw FormatException( + "Unknown version from Platform.version '$versionStringFull'."); + } + final versionString = split[1]; + final target = _dartVMstringToTarget[versionString]; + if (target == null) { + throw FormatException("Unknown ABI '$versionString' from Platform.version" + " '$versionStringFull'."); + } + return target; + } + + factory Target.fromArchitectureAndOs(Architecture architecture, OS os) { + for (final value in values) { + if (value.os == os && value.architecture == architecture) { + return value; + } + } + throw ArgumentError('Unsupported combination of OS and architecture: ' + "'${os}_$architecture'"); + } + + static const androidArm = Target._(Abi.androidArm); + static const androidArm64 = Target._(Abi.androidArm64); + static const androidIA32 = Target._(Abi.androidIA32); + static const androidX64 = Target._(Abi.androidX64); + static const androidRiscv64 = Target._(Abi.androidRiscv64); + static const fuchsiaArm64 = Target._(Abi.fuchsiaArm64); + static const fuchsiaX64 = Target._(Abi.fuchsiaX64); + static const iOSArm = Target._(Abi.iosArm); + static const iOSArm64 = Target._(Abi.iosArm64); + static const iOSX64 = Target._(Abi.iosX64); + static const linuxArm = Target._(Abi.linuxArm); + static const linuxArm64 = Target._(Abi.linuxArm64); + static const linuxIA32 = Target._(Abi.linuxIA32); + static const linuxRiscv32 = Target._(Abi.linuxRiscv32); + static const linuxRiscv64 = Target._(Abi.linuxRiscv64); + static const linuxX64 = Target._(Abi.linuxX64); + static const macOSArm64 = Target._(Abi.macosArm64); + static const macOSX64 = Target._(Abi.macosX64); + static const windowsArm64 = Target._(Abi.windowsArm64); + static const windowsIA32 = Target._(Abi.windowsIA32); + static const windowsX64 = Target._(Abi.windowsX64); + + /// All Targets that we can build for. + /// + /// Note that for some of these a Dart SDK is not available and they are only + /// used as target architectures for Flutter apps. + static const values = { + androidArm, + androidArm64, + androidIA32, + androidX64, + androidRiscv64, + fuchsiaArm64, + fuchsiaX64, + iOSArm, + iOSArm64, + iOSX64, + linuxArm, + linuxArm64, + linuxIA32, + linuxRiscv32, + linuxRiscv64, + linuxX64, + macOSArm64, + macOSX64, + windowsArm64, + windowsIA32, + windowsX64, + // TODO(dacoharkes): Add support for `wasm`. + }; + + /// Mapping from strings as used in [Target.toString] to [Target]s. + static final Map<String, Target> _stringToTarget = Map.fromEntries( + Target.values.map((target) => MapEntry(target.toString(), target))); + + /// Mapping from lowercased strings as used in [Platform.version] to + /// [Target]s. + static final Map<String, Target> _dartVMstringToTarget = Map.fromEntries( + Target.values.map((target) => MapEntry(target.dartVMToString(), target))); + + /// The current [Target]. + /// + /// Read from the [Platform.version] string. + static final Target current = Target.fromDartPlatform(Platform.version); + + Architecture get architecture => Architecture.fromAbi(abi); + + OS get os => OS.fromAbi(abi); + + String get _architectureString => architecture.dartPlatform; + + String get _osString => os.dartPlatform; + + /// A string representation of this object. + @override + String toString() => dartVMToString(); + + /// As used in [Platform.version]. + String dartVMToString() => '${_osString}_$_architectureString'; + + /// Compares `this` to [other]. + /// + /// If [other] is also an [Target], consistent with sorting on [toString]. + @override + int compareTo(Target other) => toString().compareTo(other.toString()); + + /// A list of supported target [Target]s from this host [os]. + List<Target> supportedTargetTargets( + {Map<OS, List<OS>> osCrossCompilation = + OS._osCrossCompilationDefault}) => + Target.values + .where((target) => + // Only valid cross compilation. + osCrossCompilation[os]!.contains(target.os) && + // And no deprecated architectures. + target != Target.iOSArm) + .sorted; +} + +/// Common methods for manipulating iterables of [Target]s. +extension TargetList on Iterable<Target> { + /// The [Target]s in `this` sorted by name alphabetically. + List<Target> get sorted => [for (final target in this) target]..sort(); +}
diff --git a/pkgs/native_assets_cli/lib/src/utils/datetime.dart b/pkgs/native_assets_cli/lib/src/utils/datetime.dart new file mode 100644 index 0000000..11cda00 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/datetime.dart
@@ -0,0 +1,9 @@ +// 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. + +extension DateTimeExtension on DateTime { + DateTime roundDownToSeconds() => + DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch - + millisecondsSinceEpoch % const Duration(seconds: 1).inMilliseconds); +}
diff --git a/pkgs/native_assets_cli/lib/src/utils/file.dart b/pkgs/native_assets_cli/lib/src/utils/file.dart new file mode 100644 index 0000000..703f601 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/file.dart
@@ -0,0 +1,65 @@ +// 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. + +import 'dart:convert'; +import 'dart:io'; + +extension FileExtension on File { + Future<File> writeAsStringCreateDirectory(String contents, + {FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false}) async { + if (!await parent.exists()) { + await parent.create(recursive: true); + } + return await writeAsString(contents, + mode: mode, encoding: encoding, flush: flush); + } +} + +extension FileSystemEntityExtension on FileSystemEntity { + Future<DateTime> lastModified() async { + final this_ = this; + if (this_ is Link || await FileSystemEntity.isLink(this_.path)) { + // Don't follow links. + return DateTime.fromMicrosecondsSinceEpoch(0); + } + if (this_ is File) { + if (!await this_.exists()) { + // If the file was deleted, regard it is modified recently. + return DateTime.now(); + } + return await this_.lastModified(); + } + assert(this_ is Directory); + this_ as Directory; + return await this_.lastModified(); + } +} + +extension FileSystemEntityIterable on Iterable<FileSystemEntity> { + Future<DateTime> lastModified() async { + var last = DateTime.fromMillisecondsSinceEpoch(0); + for (final entity in this) { + final entityTimestamp = await entity.lastModified(); + if (entityTimestamp.isAfter(last)) { + last = entityTimestamp; + } + } + return last; + } +} + +extension DirectoryExtension on Directory { + Future<DateTime> lastModified() async { + var last = DateTime.fromMillisecondsSinceEpoch(0); + await for (final entity in list()) { + final entityTimestamp = await entity.lastModified(); + if (entityTimestamp.isAfter(last)) { + last = entityTimestamp; + } + } + return last; + } +}
diff --git a/pkgs/native_assets_cli/lib/src/utils/map.dart b/pkgs/native_assets_cli/lib/src/utils/map.dart new file mode 100644 index 0000000..bb767ed --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/map.dart
@@ -0,0 +1,15 @@ +// 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. + +extension MapSorting<K extends Comparable<K>, V extends Object> on Map<K, V> { + Map<K, V> sortOnKey() { + final result = <K, V>{}; + final keysSorted = keys.toList()..sort(); + for (final key in keysSorted) { + final value = this[key]!; + result[key] = value; + } + return result; + } +}
diff --git a/pkgs/native_assets_cli/lib/src/utils/uri.dart b/pkgs/native_assets_cli/lib/src/utils/uri.dart new file mode 100644 index 0000000..ee4fd03 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/uri.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. + +import 'dart:io'; + +extension UriExtension on Uri { + FileSystemEntity get fileSystemEntity { + if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) { + return Directory.fromUri(this); + } + return File.fromUri(this); + } +} + +Uri fileSystemPathToUri(String path) { + if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) { + return Uri.directory(path); + } + return Uri.file(path); +}
diff --git a/pkgs/native_assets_cli/lib/src/utils/yaml.dart b/pkgs/native_assets_cli/lib/src/utils/yaml.dart new file mode 100644 index 0000000..6e1b9e3 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/yaml.dart
@@ -0,0 +1,33 @@ +// 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. + +import 'package:yaml/yaml.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +String yamlEncode(Object yamlEncoding) { + final editor = YamlEditor(''); + editor.update( + [], + wrapAsYamlNode( + yamlEncoding, + collectionStyle: CollectionStyle.BLOCK, + ), + ); + return editor.toString(); +} + +T as<T>(Object? object) { + if (object is T) { + return object; + } + throw FormatException( + "Unexpected value '$object' in YAML. Expected a $T.", + ); +} + +extension YamlMapCast on YamlMap { + Map<K, V> formatCast<K, V>() => <K, V>{ + for (final e in entries) as<K>(e.key): as<V>(e.value), + }; +}
diff --git a/pkgs/native_assets_cli/pubspec.yaml b/pkgs/native_assets_cli/pubspec.yaml new file mode 100644 index 0000000..1c944d7 --- /dev/null +++ b/pkgs/native_assets_cli/pubspec.yaml
@@ -0,0 +1,26 @@ +name: native_assets_cli +description: >- + A library that contains the argument and file formats for implementing a + native assets CLI. +version: 0.3.2 +repository: https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli + +topics: + - ffi + - interop + - native-assets + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + collection: ^1.17.1 + crypto: ^3.0.3 + pub_semver: ^2.1.3 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + +dev_dependencies: + dart_flutter_team_lints: ^2.1.1 + test: ^1.21.0
diff --git a/pkgs/native_assets_cli/test/example/native_add_library_test.dart b/pkgs/native_assets_cli/test/example/native_add_library_test.dart new file mode 100644 index 0000000..34da58d --- /dev/null +++ b/pkgs/native_assets_cli/test/example/native_add_library_test.dart
@@ -0,0 +1,92 @@ +// 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. + +@OnPlatform({ + 'mac-os': Timeout.factor(2), + 'windows': Timeout.factor(10), +}) +library; + +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() async { + late Uri tempUri; + const name = 'native_add_library'; + + setUp(() async { + tempUri = (await Directory.systemTemp.createTemp()).uri; + }); + + tearDown(() async { + await Directory.fromUri(tempUri).delete(recursive: true); + }); + + for (final dryRun in [true, false]) { + final testSuffix = dryRun ? ' dry_run' : ''; + test('native_add build$testSuffix', () async { + final testTempUri = tempUri.resolve('test1/'); + await Directory.fromUri(testTempUri).create(); + final testPackageUri = packageUri.resolve('example/$name/'); + final dartUri = Uri.file(Platform.resolvedExecutable); + + final processResult = await Process.run( + dartUri.toFilePath(), + [ + 'build.dart', + '-Dout_dir=${tempUri.toFilePath()}', + '-Dpackage_name=$name', + '-Dpackage_root=${testPackageUri.toFilePath()}', + '-Dtarget_os=${OS.current}', + '-Dversion=${BuildConfig.version}', + '-Dlink_mode_preference=dynamic', + '-Ddry_run=$dryRun', + if (!dryRun) ...[ + '-Dtarget_architecture=${Architecture.current}', + '-Dbuild_mode=debug', + if (cc != null) '-Dcc=${cc!.toFilePath()}', + if (envScript != null) + '-D${CCompilerConfig.envScriptConfigKeyFull}=' + '${envScript!.toFilePath()}', + if (envScriptArgs != null) + '-D${CCompilerConfig.envScriptArgsConfigKeyFull}=' + '${envScriptArgs!.join(' ')}', + ], + ], + workingDirectory: testPackageUri.toFilePath(), + ); + if (processResult.exitCode != 0) { + print(processResult.stdout); + print(processResult.stderr); + print(processResult.exitCode); + } + expect(processResult.exitCode, 0); + + final buildOutputUri = tempUri.resolve('build_output.yaml'); + final buildOutput = BuildOutput.fromYamlString( + await File.fromUri(buildOutputUri).readAsString()); + final assets = buildOutput.assets; + final dependencies = buildOutput.dependencies; + if (dryRun) { + expect(assets.length, greaterThanOrEqualTo(1)); + expect(await assets.first.exists(), false); + expect(dependencies.dependencies, <Uri>[]); + } else { + expect(assets.length, 1); + expect(await assets.allExist(), true); + expect( + dependencies.dependencies, + [ + testPackageUri.resolve('src/$name.c'), + testPackageUri.resolve('build.dart'), + ], + ); + } + }); + } +}
diff --git a/pkgs/native_assets_cli/test/helpers.dart b/pkgs/native_assets_cli/test/helpers.dart new file mode 100644 index 0000000..2bd2742 --- /dev/null +++ b/pkgs/native_assets_cli/test/helpers.dart
@@ -0,0 +1,105 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_cli/src/model/build_config.dart'; + +const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; + +Future<void> inTempDir( + Future<void> Function(Uri tempUri) fun, { + String? prefix, + bool keepTemp = false, +}) async { + final tempDir = await Directory.systemTemp.createTemp(prefix); + // Deal with Windows temp folder aliases. + final tempUri = + Directory(await tempDir.resolveSymbolicLinks()).uri.normalizePath(); + try { + await fun(tempUri); + } finally { + if ((!Platform.environment.containsKey(keepTempKey) || + Platform.environment[keepTempKey]!.isEmpty) && + !keepTemp) { + await tempDir.delete(recursive: true); + } + } +} + +/// Test files are run in a variety of ways, find this package root in all. +/// +/// Test files can be run from source from any working directory. The Dart SDK +/// `tools/test.py` runs them from the root of the SDK for example. +/// +/// Test files can be run from dill from the root of package. `package:test` +/// does this. +/// +/// https://github.com/dart-lang/test/issues/110 +Uri findPackageRoot(String packageName) { + final script = Platform.script; + final fileName = script.name; + if (fileName.endsWith('_test.dart')) { + // We're likely running from source. + var directory = script.resolve('.'); + while (true) { + final dirName = directory.name; + if (dirName == packageName) { + return directory; + } + final parent = directory.resolve('..'); + if (parent == directory) break; + directory = parent; + } + } else if (fileName.endsWith('.dill')) { + final cwd = Directory.current.uri; + final dirName = cwd.name; + if (dirName == packageName) { + return cwd; + } + } + throw StateError("Could not find package root for package '$packageName'. " + 'Tried finding the package root via Platform.script ' + "'${Platform.script.toFilePath()}' and Directory.current " + "'${Directory.current.uri.toFilePath()}'."); +} + +Uri packageUri = findPackageRoot('native_assets_cli'); + +extension on Uri { + String get name => pathSegments.where((e) => e != '').last; +} + +String unparseKey(String key) => key.replaceAll('.', '__').toUpperCase(); + +/// Archiver provided by the environment. +final Uri? ar = Platform + .environment[unparseKey(CCompilerConfig.arConfigKeyFull)] + ?.asFileUri(); + +/// Compiler provided by the environment. +final Uri? cc = Platform + .environment[unparseKey(CCompilerConfig.ccConfigKeyFull)] + ?.asFileUri(); + +/// Linker provided by the environment. +final Uri? ld = Platform + .environment[unparseKey(CCompilerConfig.ldConfigKeyFull)] + ?.asFileUri(); + +/// Path to script that sets environment variables for [cc], [ld], and [ar]. +/// +/// Provided by environment. +final Uri? envScript = Platform + .environment[unparseKey(CCompilerConfig.envScriptConfigKeyFull)] + ?.asFileUri(); + +/// Arguments for [envScript] provided by environment. +final List<String>? envScriptArgs = Platform + .environment[unparseKey(CCompilerConfig.envScriptArgsConfigKeyFull)] + ?.split(' '); + +extension on String { + Uri asFileUri() => Uri.file(this); +}
diff --git a/pkgs/native_assets_cli/test/model/asset_test.dart b/pkgs/native_assets_cli/test/model/asset_test.dart new file mode 100644 index 0000000..c9f2e9f --- /dev/null +++ b/pkgs/native_assets_cli/test/model/asset_test.dart
@@ -0,0 +1,196 @@ +// 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. + +import 'package:collection/collection.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + final fooUri = Uri.file('path/to/libfoo.so'); + final foo2Uri = Uri.file('path/to/libfoo2.so'); + final foo3Uri = Uri(path: 'libfoo3.so'); + final barUri = Uri(path: 'path/to/libbar.a'); + final blaUri = Uri(path: 'path/with spaces/bla.dll'); + final assets = [ + Asset( + id: 'foo', + path: AssetAbsolutePath(fooUri), + target: Target.androidX64, + linkMode: LinkMode.dynamic, + ), + Asset( + id: 'foo2', + path: AssetRelativePath(foo2Uri), + target: Target.androidX64, + linkMode: LinkMode.dynamic, + ), + Asset( + id: 'foo3', + path: AssetSystemPath(foo3Uri), + target: Target.androidX64, + linkMode: LinkMode.dynamic, + ), + Asset( + id: 'foo4', + path: AssetInExecutable(), + target: Target.androidX64, + linkMode: LinkMode.dynamic, + ), + Asset( + id: 'foo5', + path: AssetInProcess(), + target: Target.androidX64, + linkMode: LinkMode.dynamic, + ), + Asset( + id: 'bar', + path: AssetAbsolutePath(barUri), + target: Target.linuxArm64, + linkMode: LinkMode.static, + ), + Asset( + id: 'bla', + path: AssetAbsolutePath(blaUri), + target: Target.windowsX64, + linkMode: LinkMode.dynamic, + ), + ]; + + final assetsYamlEncoding = '''- id: foo + link_mode: dynamic + path: + path_type: absolute + uri: ${fooUri.toFilePath()} + target: android_x64 +- id: foo2 + link_mode: dynamic + path: + path_type: relative + uri: ${foo2Uri.toFilePath()} + target: android_x64 +- id: foo3 + link_mode: dynamic + path: + path_type: system + uri: ${foo3Uri.toFilePath()} + target: android_x64 +- id: foo4 + link_mode: dynamic + path: + path_type: executable + target: android_x64 +- id: foo5 + link_mode: dynamic + path: + path_type: process + target: android_x64 +- id: bar + link_mode: static + path: + path_type: absolute + uri: ${barUri.toFilePath()} + target: linux_arm64 +- id: bla + link_mode: dynamic + path: + path_type: absolute + uri: ${blaUri.toFilePath()} + target: windows_x64'''; + + final assetsDartEncoding = '''format-version: + - 1 + - 0 + - 0 +native-assets: + android_x64: + foo: + - absolute + - ${fooUri.toFilePath()} + foo2: + - relative + - ${foo2Uri.toFilePath()} + foo3: + - system + - ${foo3Uri.toFilePath()} + foo4: + - executable + foo5: + - process + linux_arm64: + bar: + - absolute + - ${barUri.toFilePath()} + windows_x64: + bla: + - absolute + - ${blaUri.toFilePath()}'''; + + test('asset yaml', () { + final yaml = assets.toYamlString(); + expect(yaml, assetsYamlEncoding); + final assets2 = Asset.listFromYamlString(yaml); + expect(assets, assets2); + }); + + test('asset yaml', () async { + final fileContents = assets.toNativeAssetsFile(); + expect(fileContents, assetsDartEncoding); + }); + + test('AssetPath factory', () async { + expect( + () => AssetPath('wrong', null), + throwsA(predicate( + (e) => e is FormatException && e.message.contains('Unknown pathType'), + )), + ); + }); + + test('Asset hashCode copyWith', () async { + final asset = assets.first; + final asset2 = asset.copyWith(id: 'foo321'); + expect(asset.hashCode != asset2.hashCode, true); + + final asset3 = asset.copyWith(); + expect(asset.hashCode, asset3.hashCode); + }); + + test('List<Asset> hashCode', () async { + final assets2 = assets.take(3).toList(); + const equality = ListEquality<Asset>(); + expect(equality.hash(assets) != equality.hash(assets2), true); + }); + + test('List<Asset> whereLinkMode', () async { + final assets2 = assets.whereLinkMode(LinkMode.dynamic); + expect(assets2.length, 6); + }); + + test('Asset toString', () async { + assets.toString(); + }); + + test('Asset toString', () async { + expect(await assets.allExist(), false); + }); + + test('Asset toYaml', () async { + expect( + assets.first.toYamlString(), + ''' +id: foo +link_mode: dynamic +path: + path_type: absolute + uri: ${fooUri.toFilePath()} +target: android_x64 +''' + .trim()); + }); + + test('Asset listFromYamlString', () async { + final assets = Asset.listFromYamlString(''); + expect(assets, <Asset>[]); + }); +}
diff --git a/pkgs/native_assets_cli/test/model/build_config_test.dart b/pkgs/native_assets_cli/test/model/build_config_test.dart new file mode 100644 index 0000000..9566913 --- /dev/null +++ b/pkgs/native_assets_cli/test/model/build_config_test.dart
@@ -0,0 +1,593 @@ +// 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. + +import 'dart:io'; + +import 'package:cli_config/cli_config.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() async { + late Uri tempUri; + late Uri outDirUri; + late Uri outDir2Uri; + late String packageName; + late Uri packageRootUri; + late Uri fakeClang; + late Uri fakeLd; + late Uri fakeAr; + late Uri fakeCl; + late Uri fakeVcVars; + + setUp(() async { + tempUri = (await Directory.systemTemp.createTemp()).uri; + outDirUri = tempUri.resolve('out1/'); + await Directory.fromUri(outDirUri).create(); + outDir2Uri = tempUri.resolve('out2/'); + packageName = 'my_package'; + await Directory.fromUri(outDir2Uri).create(); + packageRootUri = tempUri.resolve('$packageName/'); + await Directory.fromUri(packageRootUri).create(); + fakeClang = tempUri.resolve('fake_clang'); + await File.fromUri(fakeClang).create(); + fakeLd = tempUri.resolve('fake_ld'); + await File.fromUri(fakeLd).create(); + fakeAr = tempUri.resolve('fake_ar'); + await File.fromUri(fakeAr).create(); + fakeCl = tempUri.resolve('cl.exe'); + await File.fromUri(fakeCl).create(); + fakeVcVars = tempUri.resolve('vcvarsall.bat'); + await File.fromUri(fakeVcVars).create(); + }); + + tearDown(() async { + await Directory.fromUri(tempUri).delete(recursive: true); + }); + + test('BuildConfig ==', () { + final config1 = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.iOS, + targetIOSSdk: IOSSdk.iPhoneOs, + cCompiler: CCompilerConfig( + cc: fakeClang, + ld: fakeLd, + ar: fakeAr, + ), + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + ); + + final config2 = BuildConfig( + outDir: outDir2Uri, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.android, + targetAndroidNdkApi: 30, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + ); + + expect(config1, equals(config1)); + expect(config1 == config2, false); + expect(config1.outDir != config2.outDir, true); + expect(config1.packageRoot, config2.packageRoot); + expect(config1.targetArchitecture == config2.targetArchitecture, true); + expect(config1.targetOs != config2.targetOs, true); + expect(config1.targetIOSSdk != config2.targetIOSSdk, true); + expect(config1.cCompiler.cc != config2.cCompiler.cc, true); + expect(config1.cCompiler.ld != config2.cCompiler.ld, true); + expect(config1.cCompiler.ar != config2.cCompiler.ar, true); + expect(config1.cCompiler.envScript == config2.cCompiler.envScript, true); + expect(config1.cCompiler.envScriptArgs == config2.cCompiler.envScriptArgs, + true); + expect(config1.cCompiler != config2.cCompiler, true); + expect(config1.linkModePreference, config2.linkModePreference); + expect(config1.dependencyMetadata, config2.dependencyMetadata); + }); + + test('BuildConfig fromConfig', () { + final buildConfig2 = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: packageRootUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.android, + targetAndroidNdkApi: 30, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + ); + + final config = Config(fileParsed: { + 'build_mode': 'release', + 'dry_run': false, + 'link_mode_preference': 'prefer-static', + 'out_dir': outDirUri.toFilePath(), + 'package_name': packageName, + 'package_root': packageRootUri.toFilePath(), + 'target_android_ndk_api': 30, + 'target_architecture': 'arm64', + 'target_os': 'android', + 'version': BuildOutput.version.toString(), + }); + + final fromConfig = BuildConfig.fromConfig(config); + expect(fromConfig, equals(buildConfig2)); + }); + + test('BuildConfig.dryRun', () { + final buildConfig2 = BuildConfig.dryRun( + outDir: outDirUri, + packageName: packageName, + packageRoot: packageRootUri, + targetOs: OS.android, + linkModePreference: LinkModePreference.preferStatic, + ); + + final config = Config(fileParsed: { + 'dry_run': true, + 'link_mode_preference': 'prefer-static', + 'out_dir': outDirUri.toFilePath(), + 'package_name': packageName, + 'package_root': packageRootUri.toFilePath(), + 'target_os': 'android', + 'version': BuildOutput.version.toString(), + }); + + final fromConfig = BuildConfig.fromConfig(config); + expect(fromConfig, equals(buildConfig2)); + }); + + test('BuildConfig toYaml fromConfig', () { + final buildConfig1 = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: packageRootUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.iOS, + targetIOSSdk: IOSSdk.iPhoneOs, + cCompiler: CCompilerConfig( + cc: fakeClang, + ld: fakeLd, + ), + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + ); + + final configFile = buildConfig1.toYaml(); + final config = Config(fileParsed: configFile); + final fromConfig = BuildConfig.fromConfig(config); + expect(fromConfig, equals(buildConfig1)); + }); + + test('BuildConfig == dependency metadata', () { + final buildConfig1 = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.android, + targetAndroidNdkApi: 30, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + dependencyMetadata: { + 'bar': const Metadata({ + 'key': 'value', + 'foo': ['asdf', 'fdsa'], + }), + 'foo': const Metadata({ + 'key': 321, + }), + }, + ); + + final buildConfig2 = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.android, + targetAndroidNdkApi: 30, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + dependencyMetadata: { + 'bar': const Metadata({ + 'key': 'value', + }), + 'foo': const Metadata({ + 'key': 123, + }), + }, + ); + + expect(buildConfig1, equals(buildConfig1)); + expect(buildConfig1 == buildConfig2, false); + expect(buildConfig1.hashCode == buildConfig2.hashCode, false); + }); + + test('BuildConfig toYaml fromYaml', () { + final outDir = outDirUri; + final buildConfig1 = BuildConfig( + outDir: outDir, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.iOS, + targetIOSSdk: IOSSdk.iPhoneOs, + cCompiler: CCompilerConfig( + cc: fakeClang, + ld: fakeLd, + ), + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + // This map should be sorted on key for two layers. + dependencyMetadata: { + 'foo': const Metadata({ + 'z': ['z', 'a'], + 'a': 321, + }), + 'bar': const Metadata({ + 'key': 'value', + }), + }, + ); + final yamlString = buildConfig1.toYamlString(); + final expectedYamlString = '''build_mode: release +c_compiler: + cc: ${fakeClang.toFilePath()} + ld: ${fakeLd.toFilePath()} +dependency_metadata: + bar: + key: value + foo: + a: 321 + z: + - z + - a +link_mode_preference: prefer-static +out_dir: ${outDir.toFilePath()} +package_name: $packageName +package_root: ${tempUri.toFilePath()} +target_architecture: arm64 +target_ios_sdk: iphoneos +target_os: ios +version: ${BuildConfig.version}'''; + expect(yamlString, equals(expectedYamlString)); + + final buildConfig2 = BuildConfig.fromConfig( + Config.fromConfigFileContents( + fileContents: yamlString, + ), + ); + expect(buildConfig2, buildConfig1); + }); + + test('BuildConfig FormatExceptions', () { + expect( + () => BuildConfig.fromConfig(Config(fileParsed: {})), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + 'No value was provided for required key: build_mode', + ), + )), + ); + expect( + () => BuildConfig.fromConfig(Config(fileParsed: { + 'version': BuildConfig.version.toString(), + 'package_name': packageName, + 'package_root': packageRootUri.toFilePath(), + 'target_architecture': 'arm64', + 'target_os': 'android', + 'target_android_ndk_api': 30, + 'link_mode_preference': 'prefer-static', + })), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + 'No value was provided for required key: out_dir', + ), + )), + ); + expect( + () => BuildConfig.fromConfig(Config(fileParsed: { + 'version': BuildConfig.version.toString(), + 'out_dir': outDirUri.toFilePath(), + 'package_name': packageName, + 'package_root': packageRootUri.toFilePath(), + 'target_architecture': 'arm64', + 'target_os': 'android', + 'target_android_ndk_api': 30, + 'link_mode_preference': 'prefer-static', + 'dependency_metadata': { + 'bar': {'key': 'value'}, + 'foo': <int>[], + }, + })), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + "Unexpected value '[]' for key 'dependency_metadata.foo' in " + 'config file. Expected a Map.', + ), + )), + ); + expect( + () => BuildConfig.fromConfig(Config(fileParsed: { + 'out_dir': outDirUri.toFilePath(), + 'version': BuildConfig.version.toString(), + 'package_name': packageName, + 'package_root': packageRootUri.toFilePath(), + 'target_architecture': 'arm64', + 'target_os': 'android', + 'link_mode_preference': 'prefer-static', + })), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains( + 'No value was provided for required key: target_android_ndk_api', + ), + )), + ); + }); + + test('FormatExceptions contain full stack trace of wrapped exception', () { + try { + BuildConfig.fromConfig(Config(fileParsed: { + 'out_dir': outDirUri.toFilePath(), + 'package_root': packageRootUri.toFilePath(), + 'target': [1, 2, 3, 4, 5], + 'link_mode_preference': 'prefer-static', + })); + } on FormatException catch (e) { + expect(e.toString(), stringContainsInOrder(['Config.string'])); + } + }); + + test('BuildConfig toString', () { + final config = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.iOS, + targetIOSSdk: IOSSdk.iPhoneOs, + cCompiler: CCompilerConfig( + cc: fakeClang, + ld: fakeLd, + ), + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + ); + config.toString(); + }); + + test('BuildConfig fromArgs', () async { + final buildConfig = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.android, + targetAndroidNdkApi: 30, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + ); + final configFileContents = buildConfig.toYamlString(); + final configUri = tempUri.resolve('config.yaml'); + final configFile = File.fromUri(configUri); + await configFile.writeAsString(configFileContents); + final buildConfig2 = await BuildConfig.fromArgs( + ['--config', configUri.toFilePath()], + environment: {}, // Don't inherit the test environment. + ); + expect(buildConfig2, buildConfig); + }); + + test('dependency metadata via config accessor', () { + final buildConfig1 = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.android, + targetAndroidNdkApi: 30, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.preferStatic, + dependencyMetadata: { + 'bar': const Metadata({ + 'key': {'key2': 'value'}, + }), + }, + ); + // Useful for doing `path(..., exists: true)`. + expect( + buildConfig1.config.string([ + BuildConfig.dependencyMetadataConfigKey, + 'bar', + 'key', + 'key2' + ].join('.')), + 'value', + ); + }); + + test('envScript', () { + final buildConfig1 = BuildConfig( + outDir: outDirUri, + packageName: packageName, + packageRoot: packageRootUri, + targetArchitecture: Architecture.x64, + targetOs: OS.windows, + cCompiler: CCompilerConfig( + cc: fakeCl, + envScript: fakeVcVars, + envScriptArgs: ['x64'], + ), + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + ); + + final configFile = buildConfig1.toYaml(); + final config = Config(fileParsed: configFile); + final fromConfig = BuildConfig.fromConfig(config); + expect(fromConfig, equals(buildConfig1)); + }); + + for (final version in ['9001.0.0', '0.0.1']) { + test('BuildConfig version $version', () { + final outDir = outDirUri; + final config = Config(fileParsed: { + 'link_mode_preference': 'prefer-static', + 'out_dir': outDir.toFilePath(), + 'package_root': tempUri.toFilePath(), + 'target_os': 'linux', + 'target_architecture': 'x64', + 'version': version, + }); + expect( + () => BuildConfig.fromConfig(config), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains(version) && + e.message.contains(BuildConfig.version.toString()), + )), + ); + }); + } + + test('checksum', () async { + await inTempDir((tempUri) async { + final nativeAddUri = tempUri.resolve('native_add/'); + final fakeClangUri = tempUri.resolve('fake_clang'); + await File.fromUri(fakeClangUri).create(); + + final name1 = BuildConfig.checksum( + packageName: packageName, + packageRoot: nativeAddUri, + targetArchitecture: Architecture.x64, + targetOs: OS.linux, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + ); + + // Using the checksum for a build folder should be stable. + expect(name1, '037109b9824b2559502fa7bd42e1b6f8'); + + // Build folder different due to metadata. + final name2 = BuildConfig.checksum( + packageName: packageName, + packageRoot: nativeAddUri, + targetArchitecture: Architecture.x64, + targetOs: OS.linux, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + dependencyMetadata: { + 'foo': const Metadata({'key': 'value'}) + }, + ); + printOnFailure([name1, name2].toString()); + expect(name1 != name2, true); + + // Build folder different due to cc. + final name3 = BuildConfig.checksum( + packageName: packageName, + packageRoot: nativeAddUri, + targetArchitecture: Architecture.x64, + targetOs: OS.linux, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: fakeClangUri, + )); + printOnFailure([name1, name3].toString()); + expect(name1 != name3, true); + }); + }); + + test('BuildConfig invalid target os architecture combination', () { + final outDir = outDirUri; + final config = Config(fileParsed: { + 'link_mode_preference': 'prefer-static', + 'out_dir': outDir.toFilePath(), + 'package_name': packageName, + 'package_root': tempUri.toFilePath(), + 'target_os': 'windows', + 'target_architecture': 'arm', + 'build_mode': 'debug', + 'version': BuildConfig.version.toString(), + }); + expect( + () => BuildConfig.fromConfig(config), + throwsA(predicate( + (e) => e is FormatException && e.message.contains('arm'), + )), + ); + }); + + test('BuildConfig dry_run access invalid args', () { + final outDir = outDirUri; + final config = Config(fileParsed: { + 'link_mode_preference': 'prefer-static', + 'out_dir': outDir.toFilePath(), + 'package_name': packageName, + 'package_root': tempUri.toFilePath(), + 'target_os': 'windows', + 'target_architecture': 'arm64', + 'build_mode': 'debug', + 'dry_run': true, + 'version': BuildConfig.version.toString(), + }); + expect( + () => BuildConfig.fromConfig(config), + throwsA(predicate( + (e) => + e is FormatException && e.message.contains('In Flutter projects'), + )), + ); + }); + + test('BuildConfig dry_run access invalid args', () { + final outDir = outDirUri; + final config = Config(fileParsed: { + 'link_mode_preference': 'prefer-static', + 'out_dir': outDir.toFilePath(), + 'package_name': packageName, + 'package_root': tempUri.toFilePath(), + 'target_os': 'windows', + 'dry_run': true, + 'version': BuildConfig.version.toString(), + }); + final buildConfig = BuildConfig.fromConfig(config); + expect( + () => buildConfig.targetArchitecture, + throwsA(predicate( + (e) => e is StateError && e.message.contains('In Flutter projects'), + )), + ); + }); + + test('BuildConfig dry_run access invalid args', () { + final buildConfig = BuildConfig.dryRun( + packageName: packageName, + outDir: outDirUri, + packageRoot: tempUri, + targetOs: OS.windows, + linkModePreference: LinkModePreference.dynamic, + ); + buildConfig.toYamlString(); + // No crash. + }); +}
diff --git a/pkgs/native_assets_cli/test/model/build_output_test.dart b/pkgs/native_assets_cli/test/model/build_output_test.dart new file mode 100644 index 0000000..0dc0bad --- /dev/null +++ b/pkgs/native_assets_cli/test/model/build_output_test.dart
@@ -0,0 +1,173 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + late Uri tempUri; + + setUp(() async { + tempUri = (await Directory.systemTemp.createTemp()).uri; + }); + + tearDown(() async { + await Directory.fromUri(tempUri).delete(recursive: true); + }); + + final buildOutput = BuildOutput( + timestamp: DateTime.parse('2022-11-10 13:25:01.000'), + assets: [ + Asset( + id: 'foo', + path: AssetAbsolutePath(Uri(path: 'path/to/libfoo.so')), + target: Target.androidX64, + linkMode: LinkMode.dynamic, + ), + Asset( + id: 'foo2', + path: AssetRelativePath(Uri(path: 'path/to/libfoo2.so')), + target: Target.androidX64, + linkMode: LinkMode.dynamic, + ), + ], + dependencies: Dependencies([ + Uri.file('path/to/file.ext'), + ]), + metadata: const Metadata({ + 'key': 'value', + }), + ); + + final yamlEncoding = '''timestamp: 2022-11-10 13:25:01.000 +assets: + - id: foo + link_mode: dynamic + path: + path_type: absolute + uri: path/to/libfoo.so + target: android_x64 + - id: foo2 + link_mode: dynamic + path: + path_type: relative + uri: path/to/libfoo2.so + target: android_x64 +dependencies: + - path/to/file.ext +metadata: + key: value +version: ${BuildOutput.version}'''; + + test('built info yaml', () { + final yaml = buildOutput.toYamlString().replaceAll('\\', '/'); + expect(yaml, yamlEncoding); + final buildOutput2 = BuildOutput.fromYamlString(yaml); + expect(buildOutput.hashCode, buildOutput2.hashCode); + expect(buildOutput, buildOutput2); + }); + + test('BuildOutput.toString', buildOutput.toString); + + test('BuildOutput.hashCode', () { + final buildOutput2 = BuildOutput.fromYamlString(yamlEncoding); + expect(buildOutput.hashCode, buildOutput2.hashCode); + + final buildOutput3 = BuildOutput( + timestamp: DateTime.parse('2022-11-10 13:25:01.000'), + ); + expect(buildOutput.hashCode != buildOutput3.hashCode, true); + }); + + test('BuildOutput.readFromFile BuildOutput.writeToFile', () async { + final outDir = tempUri.resolve('out_dir/'); + await buildOutput.writeToFile(outDir: outDir); + final buildOutput2 = await BuildOutput.readFromFile(outDir: outDir); + expect(buildOutput2, buildOutput); + }); + + test('Round timestamp', () { + final buildOutput3 = BuildOutput( + timestamp: DateTime.parse('2022-11-10 13:25:01.372257'), + ); + expect(buildOutput3.timestamp, DateTime.parse('2022-11-10 13:25:01.000')); + }); + + for (final version in ['9001.0.0', '0.0.1']) { + test('BuildOutput version $version', () { + expect( + () => BuildOutput.fromYamlString('version: $version'), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains(version) && + e.message.contains(BuildConfig.version.toString()), + )), + ); + }); + } + + test('format exception', () { + expect( + () => BuildOutput.fromYamlString('''timestamp: 2022-11-10 13:25:01.000 +assets: + - name: foo + link_mode: dynamic + path: + path_type: + some: map + uri: path/to/libfoo.so + target: android_x64 +dependencies: [] +metadata: + key: value +version: ${BuildOutput.version}'''), + throwsFormatException, + ); + expect( + () => BuildOutput.fromYamlString('''timestamp: 2022-11-10 13:25:01.000 +assets: + - name: foo + link_mode: dynamic + path: + path_type: absolute + uri: path/to/libfoo.so + target: android_x64 +dependencies: + 1: foo +metadata: + key: value +version: ${BuildOutput.version}'''), + throwsFormatException, + ); + expect( + () => BuildOutput.fromYamlString('''timestamp: 2022-11-10 13:25:01.000 +assets: + - name: foo + link_mode: dynamic + path: + path_type: absolute + uri: path/to/libfoo.so + target: android_x64 +dependencies: [] +metadata: + 123: value +version: ${BuildOutput.version}'''), + throwsFormatException, + ); + }); + + test('BuildOutput dependencies can be modified', () { + // TODO(https://github.com/dart-lang/native/issues/25): + // Remove once dependencies are made immutable. + final buildOutput = BuildOutput(); + expect( + () => buildOutput.dependencies.dependencies + .add(Uri.file('path/to/file.ext')), + returnsNormally, + ); + }); +}
diff --git a/pkgs/native_assets_cli/test/model/dependencies_test.dart b/pkgs/native_assets_cli/test/model/dependencies_test.dart new file mode 100644 index 0000000..a4f05c5 --- /dev/null +++ b/pkgs/native_assets_cli/test/model/dependencies_test.dart
@@ -0,0 +1,92 @@ +// 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. + +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + late Uri tempUri; + + setUp(() async => tempUri = Directory( + await (await Directory.systemTemp.createTemp()) + .resolveSymbolicLinks()) + .uri); + + tearDown( + () async => await Directory.fromUri(tempUri).delete(recursive: true)); + + final dependencies = Dependencies([ + Uri.file('src/bar.c'), + Uri.file('src/baz.c'), + Uri.directory('src/bla/'), + Uri.file('build.dart'), + ]); + + const yamlEncoding = '''- src/bar.c +- src/baz.c +- src/bla/ +- build.dart'''; + + test('dependencies yaml', () { + final yaml = dependencies.toYamlString().replaceAll('\\', '/'); + expect(yaml, yamlEncoding); + final dependencies2 = Dependencies.fromYamlString(yaml); + expect(dependencies.hashCode, dependencies2.hashCode); + expect(dependencies, dependencies2); + }); + + test('dependencies toString', dependencies.toString); + + test('dependencies fromYamlString', () { + final dependencies = Dependencies.fromYamlString(''); + expect(dependencies, const Dependencies([])); + }); + + test('dependencies lastModified', () async { + final dirUri = tempUri.resolve('foo/'); + final dir = Directory.fromUri(dirUri); + await dir.create(); + final fileUri = tempUri.resolve('bla.c'); + final file = File.fromUri(fileUri); + await file.writeAsString('dummy contents'); + final dependencies = Dependencies([dirUri, fileUri]); + expect(await dependencies.lastModified(), await file.lastModified()); + }); + + test('dependencies lastModified symlinks', () async { + if (Platform.isWindows) { + // Requires extra privilege, skip. + // "A required privilege is not held by the client" + return; + } + final symlink = Link.fromUri(tempUri.resolve('my_link')); + await symlink.create(tempUri.toFilePath()); + + final someFileUri = tempUri.resolve('foo.txt'); + final someFile = File.fromUri(someFileUri); + await someFile.writeAsString('yay!'); + + final dependencies = Dependencies([tempUri]); + expect(await dependencies.lastModified(), await someFile.lastModified()); + }); + + test('dependencies lastModified does not exist', () async { + final someFileUri = tempUri.resolve('foo.txt'); + final someFile = File.fromUri(someFileUri); + await someFile.writeAsString('yay!'); + + final deletedFileUri = tempUri.resolve('bar.txt'); + + final now = DateTime.now(); + + final dependencies = Dependencies([ + someFileUri, + deletedFileUri, + ]); + final depsLastModified = await dependencies.lastModified(); + expect(depsLastModified == now || depsLastModified.isAfter(now), true); + }); +}
diff --git a/pkgs/native_assets_cli/test/model/link_mode_test.dart b/pkgs/native_assets_cli/test/model/link_mode_test.dart new file mode 100644 index 0000000..ba86d44 --- /dev/null +++ b/pkgs/native_assets_cli/test/model/link_mode_test.dart
@@ -0,0 +1,12 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + test('LinkMode toString', () async { + LinkMode.static.toString(); + }); +}
diff --git a/pkgs/native_assets_cli/test/model/metadata_test.dart b/pkgs/native_assets_cli/test/model/metadata_test.dart new file mode 100644 index 0000000..fd80b68 --- /dev/null +++ b/pkgs/native_assets_cli/test/model/metadata_test.dart
@@ -0,0 +1,25 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + const metadata = Metadata({ + 'key': 'value', + 'my_list': [1, 2, 3], + 'my_map': { + 3: 4, + 'foo': 'bar', + }, + }); + + test('Metadata toString', metadata.toString); + + test('Metadata toYamlString fromYamlString', () { + final yamlString = metadata.toYamlString(); + final metadata2 = Metadata.fromYamlString(yamlString); + expect(metadata2, metadata); + }); +}
diff --git a/pkgs/native_assets_cli/test/model/target_test.dart b/pkgs/native_assets_cli/test/model/target_test.dart new file mode 100644 index 0000000..3b1ce7b --- /dev/null +++ b/pkgs/native_assets_cli/test/model/target_test.dart
@@ -0,0 +1,75 @@ +// 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. + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + test('OS naming conventions', () async { + expect(OS.android.dylibFileName('foo'), 'libfoo.so'); + expect(OS.android.staticlibFileName('foo'), 'libfoo.a'); + expect(OS.windows.dylibFileName('foo'), 'foo.dll'); + expect(OS.windows.libraryFileName('foo', LinkMode.dynamic), 'foo.dll'); + expect(OS.windows.staticlibFileName('foo'), 'foo.lib'); + expect(OS.windows.libraryFileName('foo', LinkMode.static), 'foo.lib'); + expect(OS.windows.executableFileName('foo'), 'foo.exe'); + }); + + test('Target current', () async { + final current = Target.current; + expect(current.toString(), Abi.current().toString()); + }); + + test('Target fromDartPlatform', () async { + final current = Target.fromDartPlatform(Platform.version); + expect(current.toString(), Abi.current().toString()); + expect( + () => Target.fromDartPlatform('bogus'), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains('bogus') && + e.message.contains('Unknown version'), + )), + ); + expect( + () => Target.fromDartPlatform( + '3.0.0 (be) (Wed Apr 5 14:19:42 2023 +0000) on "myfancyos_ia32"', + ), + throwsA(predicate( + (e) => + e is FormatException && + e.message.contains('myfancyos_ia32') && + e.message.contains('Unknown ABI'), + )), + ); + }); + + test('Target cross compilation', () async { + // All hosts can cross compile to Android. + expect( + Target.current.supportedTargetTargets(), contains(Target.androidArm64)); + expect( + Target.macOSArm64.supportedTargetTargets(), contains(Target.iOSArm64)); + }); + + test('Target fromArchitectureAndOs', () async { + final current = + Target.fromArchitectureAndOs(Architecture.current, OS.current); + expect(current.toString(), Abi.current().toString()); + + expect( + () => Target.fromArchitectureAndOs(Architecture.arm, OS.windows), + throwsA(predicate( + (e) => + e is ArgumentError && + (e.message as String).contains('arm') && + (e.message as String).contains('windows'), + )), + ); + }); +}
diff --git a/pkgs/native_toolchain_c/.gitignore b/pkgs/native_toolchain_c/.gitignore new file mode 100644 index 0000000..58e48f3 --- /dev/null +++ b/pkgs/native_toolchain_c/.gitignore
@@ -0,0 +1,9 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +coverage/
diff --git a/pkgs/native_toolchain_c/CHANGELOG.md b/pkgs/native_toolchain_c/CHANGELOG.md new file mode 100644 index 0000000..c347917 --- /dev/null +++ b/pkgs/native_toolchain_c/CHANGELOG.md
@@ -0,0 +1,52 @@ +## 0.3.2 + +- Added workaround minSdkVersion 19 and 20 for Android. +- Start using sysroot for Android. +- Added tests for up to Android API version 34. + +## 0.3.1 + +- Added MSVC arm64 toolchain. + +## 0.3.0 + +- Bump `package:native_assets_cli` to 0.3.0. + +## 0.2.5 + +- Explicitly tell linker to create position dependent or position independent executable + ([#113](https://github.com/dart-lang/native/issues/133)). + +## 0.2.4 + +- Added `includes` for specifying include directories. +- Added `flags` for specifying arbitrary compiler flags. +- Added `std` for specifying a language standard. +- Added `language` for selecting the language (`c` and `cpp`) to compile source files as. +- Added `cppLinkStdLib` for specifying the C++ standard library to link against. + +## 0.2.3 + +- Fix MSVC tool resolution inside (x86) folder + ([#123](https://github.com/dart-lang/native/issues/123)). + +## 0.2.2 + +- Generate position independent code for libraries by default and add + `pic` option to control this behavior. + +## 0.2.1 + +- Added `defines` for specifying custom defines. +- Added `buildModeDefine` to toggle define for current build mode. +- Added `ndebugDefine` to toggle define of `NDEBUG` for non-debug builds. + +## 0.2.0 + +- **Breaking change** Rename `assetName` to `assetId` + ([#100](https://github.com/dart-lang/native/issues/100)). +- Added topics. + +## 0.1.0 + +- Initial version.
diff --git a/pkgs/native_toolchain_c/LICENSE b/pkgs/native_toolchain_c/LICENSE new file mode 100644 index 0000000..4fd5739 --- /dev/null +++ b/pkgs/native_toolchain_c/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2023, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/native_toolchain_c/README.md b/pkgs/native_toolchain_c/README.md new file mode 100644 index 0000000..01b1448 --- /dev/null +++ b/pkgs/native_toolchain_c/README.md
@@ -0,0 +1,26 @@ +[](https://github.com/dart-lang/native/actions/workflows/native.yaml) +[](https://coveralls.io/github/dart-lang/native?branch=main) +[](https://pub.dev/packages/native_toolchain_c) +[](https://pub.dev/packages/native_toolchain_c/publisher) + +A library to invoke the native C compiler installed on the host machine. + +## Status: Experimental + +**NOTE**: This package is currently experimental and published under the +[labs.dart.dev](https://dart.dev/dart-team-packages) pub publisher in order to +solicit feedback. + +For packages in the labs.dart.dev publisher we generally plan to either graduate +the package into a supported publisher (dart.dev, tools.dart.dev) after a period +of feedback and iteration, or discontinue the package. These packages have a +much higher expected rate of API and breaking changes. + +Your feedback is valuable and will help us evolve this package. +For bugs, please file an issue in the +[bug tracker](https://github.com/dart-lang/native/issues). + + +## Example + +An example can be found in [../native_assets_cli/example/](../native_assets_cli/example/).
diff --git a/pkgs/native_toolchain_c/analysis_options.yaml b/pkgs/native_toolchain_c/analysis_options.yaml new file mode 100644 index 0000000..dd3dcda --- /dev/null +++ b/pkgs/native_toolchain_c/analysis_options.yaml
@@ -0,0 +1,12 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-raw-types: true + +linter: + rules: + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_in_for_each + - prefer_final_locals
diff --git a/pkgs/native_toolchain_c/lib/native_toolchain_c.dart b/pkgs/native_toolchain_c/lib/native_toolchain_c.dart new file mode 100644 index 0000000..f6f90d1 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/native_toolchain_c.dart
@@ -0,0 +1,8 @@ +// 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. + +/// A library to invoke the native C compiler installed on the host machine. +library; + +export 'src/cbuilder/cbuilder.dart';
diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart new file mode 100644 index 0000000..a0a199b --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart
@@ -0,0 +1,289 @@ +// 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. + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import 'run_cbuilder.dart'; + +abstract class Builder { + Future<void> run({ + required BuildConfig buildConfig, + required BuildOutput buildOutput, + required Logger? logger, + }); +} + +/// 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++'); + + /// Known values for [Language]. + static const List<Language> values = [c, cpp]; + + @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.target] and + /// [OS.libraryFileName] or [OS.executableFileName]. + /// + /// File will be placed in [BuildConfig.outDir]. + final String name; + + /// Asset identifier. + /// + /// Used to output the [BuildOutput.assets]. + /// + /// If omitted, no asset will be added to the build output. + final String? assetId; + + /// Sources to build the library or executable. + /// + /// Resolved against [BuildConfig.packageRoot]. + /// + /// Used to output the [BuildOutput.dependencies]. + final List<String> sources; + + /// Include directories to pass to the compiler. + /// + /// Resolved against [BuildConfig.packageRoot]. + /// + /// Used to output the [BuildOutput.dependencies]. + final List<String> includes; + + /// The dart files involved in building this artifact. + /// + /// Resolved against [BuildConfig.packageRoot]. + /// + /// Used to output the [BuildOutput.dependencies]. + final List<String> 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<String> flags; + + /// Definitions of preprocessor macros. + /// + /// When the value is `null`, the macro is defined without a value. + final Map<String, String?> 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 + /// value. + /// + /// Defaults to `true`. + final bool buildModeDefine; + + /// Whether to define the standard `NDEBUG` macro when _not_ building with + /// [BuildMode.debug]. + /// + /// When `NDEBUG` is defined, the C/C++ standard library + /// [`assert` macro in `assert.h`](https://en.wikipedia.org/wiki/Assert.h) + /// becomes a no-op. Other C/C++ code commonly use `NDEBUG` to disable debug + /// features, as well. + /// + /// 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; + + CBuilder.library({ + required this.name, + required this.assetId, + this.sources = const [], + this.includes = const [], + this.dartBuildFiles = const ['build.dart'], + @visibleForTesting this.installName, + this.flags = const [], + this.defines = const {}, + this.buildModeDefine = true, + this.ndebugDefine = true, + this.pic = true, + this.std, + this.language = Language.c, + this.cppLinkStdLib, + }) : _type = _CBuilderType.library; + + CBuilder.executable({ + required this.name, + this.sources = const [], + this.includes = const [], + this.dartBuildFiles = const ['build.dart'], + this.flags = const [], + this.defines = const {}, + this.buildModeDefine = true, + this.ndebugDefine = true, + bool? pie = false, + this.std, + this.language = Language.c, + this.cppLinkStdLib, + }) : _type = _CBuilderType.executable, + assetId = null, + installName = null, + pic = pie; + + /// Runs the C Compiler with on this C build spec. + /// + /// Completes with an error if the build fails. + @override + Future<void> run({ + required BuildConfig buildConfig, + required BuildOutput buildOutput, + required Logger? logger, + }) async { + final outDir = buildConfig.outDir; + final packageRoot = buildConfig.packageRoot; + await Directory.fromUri(outDir).create(recursive: true); + final linkMode = buildConfig.linkModePreference.preferredLinkMode; + final libUri = + outDir.resolve(buildConfig.targetOs.libraryFileName(name, linkMode)); + final exeUri = + outDir.resolve(buildConfig.targetOs.executableFileName(name)); + 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)), + ]; + final dartBuildFiles = [ + for (final source in this.dartBuildFiles) packageRoot.resolve(source), + ]; + if (!buildConfig.dryRun) { + final task = RunCBuilder( + buildConfig: buildConfig, + logger: logger, + sources: sources, + includes: includes, + dynamicLibrary: + _type == _CBuilderType.library && linkMode == LinkMode.dynamic + ? libUri + : null, + staticLibrary: + _type == _CBuilderType.library && linkMode == LinkMode.static + ? libUri + : null, + executable: _type == _CBuilderType.executable ? exeUri : null, + installName: installName, + flags: flags, + defines: { + ...defines, + if (buildModeDefine) buildConfig.buildMode.name.toUpperCase(): null, + if (ndebugDefine && buildConfig.buildMode != BuildMode.debug) + 'NDEBUG': null, + }, + pic: pic, + std: std, + language: language, + cppLinkStdLib: cppLinkStdLib, + ); + await task.run(); + } + + if (assetId != null) { + final targets = [ + if (!buildConfig.dryRun) + buildConfig.target + else + for (final target in Target.values) + if (target.os == buildConfig.targetOs) target + ]; + for (final target in targets) { + buildOutput.assets.add(Asset( + id: assetId!, + linkMode: linkMode, + target: target, + path: AssetAbsolutePath(libUri), + )); + } + } + if (!buildConfig.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(); + + buildOutput.dependencies.dependencies.addAll({ + // Note: We use a Set here to deduplicate the dependencies. + ...sources, + ...includeFiles, + ...dartBuildFiles, + }); + } + } +} + +enum _CBuilderType { + executable, + library, +}
diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart new file mode 100644 index 0000000..17ec90a --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart
@@ -0,0 +1,226 @@ +// 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. + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../native_toolchain/android_ndk.dart'; +import '../native_toolchain/apple_clang.dart'; +import '../native_toolchain/clang.dart'; +import '../native_toolchain/gcc.dart'; +import '../native_toolchain/msvc.dart'; +import '../native_toolchain/recognizer.dart'; +import '../tool/tool.dart'; +import '../tool/tool_error.dart'; +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 Logger? logger; + final Target host; + + CompilerResolver({ + required this.buildConfig, + required this.logger, + Target? host, // Only visible for testing. + }) : host = host ?? Target.current; + + Future<ToolInstance> resolveCompiler() async { + // First, check if the launcher provided a direct path to the compiler. + var result = await _tryLoadCompilerFromConfig( + CCompilerConfig.ccConfigKeyFull, + (buildConfig) => buildConfig.cCompiler.cc, + ); + + // Then, try to detect on the host machine. + final tool = _selectCompiler(); + if (tool != null) { + result ??= await _tryLoadToolFromNativeToolchain(tool); + } + + if (result != null) { + return result; + } + + final targetOs = buildConfig.targetOs; + final targetArchitecture = buildConfig.targetArchitecture; + final errorMessage = "No tools configured on host '$host' with target " + "'${targetOs}_$targetArchitecture'."; + logger?.severe(errorMessage); + throw ToolError(errorMessage); + } + + /// Select the right compiler for cross compiling to the specified target. + Tool? _selectCompiler() { + final targetOs = buildConfig.targetOs; + final targetArch = buildConfig.targetArchitecture; + + // TODO(dacoharkes): Support falling back on other tools. + if (targetArch == host.architecture && + targetOs == host.os && + host.os == OS.linux) return clang; + if (targetOs == OS.macOS || targetOs == OS.iOS) return appleClang; + if (targetOs == OS.android) return androidNdkClang; + if (host.os == OS.linux) { + switch (targetArch) { + case Architecture.arm: + return armLinuxGnueabihfGcc; + case Architecture.arm64: + return aarch64LinuxGnuGcc; + case Architecture.ia32: + return i686LinuxGnuGcc; + case Architecture.x64: + return x86_64LinuxGnuGcc; + case Architecture.riscv64: + return riscv64LinuxGnuGcc; + } + } + + if (host.os == OS.windows) { + switch (targetArch) { + case Architecture.ia32: + return clIA32; + case Architecture.arm64: + return clArm64; + case Architecture.x64: + return cl; + } + } + + return null; + } + + Future<ToolInstance?> _tryLoadCompilerFromConfig( + String configKey, Uri? Function(BuildConfig) getter) async { + final configCcUri = getter(buildConfig); + if (configCcUri != null) { + assert(await File.fromUri(configCcUri).exists()); + logger?.finer('Using compiler ${configCcUri.toFilePath()} ' + 'from config[${CCompilerConfig.ccConfigKeyFull}].'); + return (await CompilerRecognizer(configCcUri).resolve(logger: logger)) + .first; + } + logger?.finer( + 'No compiler set in config[${CCompilerConfig.ccConfigKeyFull}].'); + return null; + } + + Future<ToolInstance?> _tryLoadToolFromNativeToolchain(Tool tool) async { + final resolved = (await tool.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == tool) + .toList() + ..sort(); + return resolved.isEmpty ? null : resolved.first; + } + + Future<ToolInstance> resolveArchiver() async { + // First, check if the launcher provided a direct path to the compiler. + var result = await _tryLoadArchiverFromConfig( + CCompilerConfig.arConfigKeyFull, + (buildConfig) => buildConfig.cCompiler.ar, + ); + + // Then, try to detect on the host machine. + final tool = _selectArchiver(); + if (tool != null) { + result ??= await _tryLoadToolFromNativeToolchain(tool); + } + + if (result != null) { + return result; + } + + final targetOs = buildConfig.targetOs; + final targetArchitecture = buildConfig.targetArchitecture; + final errorMessage = "No tools configured on host '$host' with target " + "'${targetOs}_$targetArchitecture'."; + logger?.severe(errorMessage); + throw ToolError(errorMessage); + } + + /// Select the right archiver for cross compiling to the specified target. + Tool? _selectArchiver() { + final targetOs = buildConfig.targetOs; + final targetArchitecture = buildConfig.targetArchitecture; + + // TODO(dacoharkes): Support falling back on other tools. + if (targetArchitecture == host.architecture && + targetOs == host.os && + host.os == OS.linux) { + return llvmAr; + } + if (targetOs == OS.macOS || targetOs == OS.iOS) return appleAr; + if (targetOs == OS.android) return androidNdkLlvmAr; + if (host.os == OS.linux) { + switch (targetArchitecture) { + case Architecture.arm: + return armLinuxGnueabihfGccAr; + case Architecture.arm64: + return aarch64LinuxGnuGccAr; + case Architecture.ia32: + return i686LinuxGnuGccAr; + case Architecture.x64: + return x86_64LinuxGnuGccAr; + case Architecture.riscv64: + return riscv64LinuxGnuGccAr; + } + } + if (host.os == OS.windows) { + switch (targetArchitecture) { + case Architecture.ia32: + return libIA32; + case Architecture.arm64: + return libArm64; + case Architecture.x64: + return lib; + } + } + + return null; + } + + Future<ToolInstance?> _tryLoadArchiverFromConfig( + String configKey, Uri? Function(BuildConfig) getter) async { + final configArUri = getter(buildConfig); + if (configArUri != null) { + assert(await File.fromUri(configArUri).exists()); + logger?.finer('Using archiver ${configArUri.toFilePath()} ' + 'from config[${CCompilerConfig.arConfigKeyFull}].'); + return (await ArchiverRecognizer(configArUri).resolve(logger: logger)) + .first; + } + logger?.finer( + 'No archiver set in config[${CCompilerConfig.arConfigKeyFull}].'); + return null; + } + + Future<Uri?> toolchainEnvironmentScript(ToolInstance compiler) async { + final fromConfig = buildConfig.cCompiler.envScript; + if (fromConfig != null) { + logger?.fine('Using envScript from config: $fromConfig'); + return fromConfig; + } + + final compilerTool = compiler.tool; + assert(compilerTool == cl); + final vcvarsScript = + (await vcvars(compiler).defaultResolver!.resolve(logger: logger)).first; + return vcvarsScript.uri; + } + + List<String>? toolchainEnvironmentScriptArguments() { + final fromConfig = buildConfig.cCompiler.envScriptArgs; + if (fromConfig != null) { + logger?.fine('Using envScriptArgs from config: $fromConfig'); + return fromConfig; + } + + // vcvars above already has x64 or x86 in the script name. + return null; + } +}
diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart new file mode 100644 index 0000000..0db3dda --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart
@@ -0,0 +1,321 @@ +// 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. + +import 'dart:math'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../native_toolchain/apple_clang.dart'; +import '../native_toolchain/clang.dart'; +import '../native_toolchain/gcc.dart'; +import '../native_toolchain/msvc.dart'; +import '../native_toolchain/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'; + +class RunCBuilder { + final BuildConfig buildConfig; + final Logger? logger; + final List<Uri> sources; + final List<Uri> includes; + final Uri? executable; + final Uri? dynamicLibrary; + final Uri? staticLibrary; + final Uri outDir; + final Target target; + + /// The install name of the [dynamicLibrary]. + /// + /// Can be inspected with `otool -D <path-to-dylib>`. + /// + /// Can be modified with `install_name_tool`. + final Uri? installName; + + final List<String> flags; + final Map<String, String?> defines; + final bool? pic; + final String? std; + final Language language; + final String? cppLinkStdLib; + + RunCBuilder({ + required this.buildConfig, + this.logger, + this.sources = const [], + this.includes = const [], + this.executable, + this.dynamicLibrary, + this.staticLibrary, + this.installName, + this.flags = const [], + this.defines = const {}, + this.pic, + this.std, + this.language = Language.c, + this.cppLinkStdLib, + }) : outDir = buildConfig.outDir, + target = buildConfig.target, + assert([executable, dynamicLibrary, staticLibrary] + .whereType<Uri>() + .length == + 1) { + if (target.os == OS.windows && cppLinkStdLib != null) { + throw ArgumentError.value( + cppLinkStdLib, + 'cppLinkStdLib', + 'is not supported when targeting Windows', + ); + } + } + + late final _resolver = + CompilerResolver(buildConfig: buildConfig, logger: logger); + + Future<ToolInstance> compiler() async => await _resolver.resolveCompiler(); + + Future<Uri> archiver() async => (await _resolver.resolveArchiver()).uri; + + Future<Uri> iosSdk(IOSSdk iosSdk, {required Logger? logger}) async { + if (iosSdk == IOSSdk.iPhoneOs) { + return (await iPhoneOSSdk.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == iPhoneOSSdk) + .first + .uri; + } + assert(iosSdk == IOSSdk.iPhoneSimulator); + return (await iPhoneSimulatorSdk.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == iPhoneSimulatorSdk) + .first + .uri; + } + + Future<Uri> macosSdk({required Logger? logger}) async => + (await macosxSdk.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == macosxSdk) + .first + .uri; + + Uri androidSysroot(ToolInstance compiler) => + compiler.uri.resolve('../sysroot/'); + + Future<void> run() async { + final compiler_ = await compiler(); + final compilerTool = compiler_.tool; + if (compilerTool == appleClang || + compilerTool == clang || + compilerTool == gcc) { + await runClangLike(compiler: compiler_); + return; + } + assert(compilerTool == cl); + await runCl(compiler: compiler_); + } + + Future<void> runClangLike({required ToolInstance compiler}) async { + final isStaticLib = staticLibrary != null; + Uri? archiver_; + if (isStaticLib) { + archiver_ = await archiver(); + } + + late final IOSSdk targetIosSdk; + if (target.os == OS.iOS) { + targetIosSdk = buildConfig.targetIOSSdk!; + } + + // The Android Gradle plugin does not honor API level 19 and 20 when + // invoking clang. Mimic that behavior here. + // See https://github.com/dart-lang/native/issues/171. + late final int targetAndroidNdkApi; + if (target.os == OS.android) { + targetAndroidNdkApi = max(buildConfig.targetAndroidNdkApi!, 21); + } + + await runProcess( + executable: compiler.uri, + arguments: [ + if (target.os == OS.android) ...[ + '--target=' + '${androidNdkClangTargetFlags[target]!}' + '$targetAndroidNdkApi', + '--sysroot=${androidSysroot(compiler).toFilePath()}', + ], + if (target.os == OS.macOS) + '--target=${appleClangMacosTargetFlags[target]!}', + if (target.os == OS.iOS) + '--target=${appleClangIosTargetFlags[target]![targetIosSdk]!}', + if (target.os == OS.iOS) ...[ + '-isysroot', + (await iosSdk(targetIosSdk, logger: logger)).toFilePath(), + ], + if (target.os == OS.macOS) ...[ + '-isysroot', + (await macosSdk(logger: logger)).toFilePath(), + ], + if (installName != null) ...[ + '-install_name', + installName!.toFilePath(), + ], + if (pic != null) + if (pic!) ...[ + if (dynamicLibrary != null) '-fPIC', + // Using PIC for static libraries allows them to be linked into + // any executable, but it is not necessarily the best option in + // terms of overhead. We would have to know wether the target into + // which the static library is linked is PIC, PIE or neither. Then + // we could use the same option for the static library. + if (staticLibrary != null) '-fPIC', + if (executable != null) ...[ + // Generate position-independent code for executables. + '-fPIE', + // Tell the linker to generate a position-independent executable. + '-pie', + ], + ] else ...[ + // Disable generation of any kind of position-independent code. + '-fno-PIC', + '-fno-PIE', + // Tell the linker to generate a position-dependent executable. + if (executable != null) '-no-pie', + ], + if (std != null) '-std=$std', + if (language == Language.cpp) ...[ + '-x', + 'c++', + '-l', + cppLinkStdLib ?? defaultCppLinkStdLib[target.os]! + ], + ...flags, + for (final MapEntry(key: name, :value) in defines.entries) + if (value == null) '-D$name' else '-D$name=$value', + for (final include in includes) '-I${include.toFilePath()}', + ...sources.map((e) => e.toFilePath()), + if (executable != null) ...[ + '-o', + outDir.resolveUri(executable!).toFilePath(), + ], + if (dynamicLibrary != null) ...[ + '--shared', + '-o', + outDir.resolveUri(dynamicLibrary!).toFilePath(), + ] else if (staticLibrary != null) ...[ + '-c', + '-o', + outDir.resolve('out.o').toFilePath(), + ], + ], + logger: logger, + captureOutput: false, + throwOnUnexpectedExitCode: true, + ); + if (staticLibrary != null) { + await runProcess( + executable: archiver_!, + arguments: [ + 'rc', + outDir.resolveUri(staticLibrary!).toFilePath(), + outDir.resolve('out.o').toFilePath(), + ], + logger: logger, + captureOutput: false, + throwOnUnexpectedExitCode: true, + ); + } + } + + Future<void> runCl({required ToolInstance compiler}) async { + final vcvars = (await _resolver.toolchainEnvironmentScript(compiler))!; + final vcvarsArgs = _resolver.toolchainEnvironmentScriptArguments(); + final environment = await envFromBat(vcvars, arguments: vcvarsArgs ?? []); + + final isStaticLib = staticLibrary != null; + Uri? archiver_; + if (isStaticLib) { + archiver_ = await archiver(); + } + + final result = await runProcess( + executable: compiler.uri, + arguments: [ + if (std != null) '/std:$std', + if (language == Language.cpp) '/TP', + ...flags, + for (final MapEntry(key: name, :value) in defines.entries) + if (value == null) '/D$name' else '/D$name=$value', + for (final directory in includes) '/I${directory.toFilePath()}', + if (executable != null) ...[ + ...sources.map((e) => e.toFilePath()), + '/link', + '/out:${outDir.resolveUri(executable!).toFilePath()}', + ], + if (dynamicLibrary != null) ...[ + ...sources.map((e) => e.toFilePath()), + '/link', + '/DLL', + '/out:${outDir.resolveUri(dynamicLibrary!).toFilePath()}', + ], + if (staticLibrary != null) ...[ + '/c', + ...sources.map((e) => e.toFilePath()), + ], + ], + workingDirectory: outDir, + environment: environment, + logger: logger, + captureOutput: false, + throwOnUnexpectedExitCode: true, + ); + + if (staticLibrary != null) { + await runProcess( + executable: archiver_!, + arguments: [ + '/out:${staticLibrary!.toFilePath()}', + '*.obj', + ], + workingDirectory: outDir, + environment: environment, + logger: logger, + captureOutput: false, + throwOnUnexpectedExitCode: true, + ); + } + + assert(result.exitCode == 0); + } + + static const androidNdkClangTargetFlags = { + Target.androidArm: 'armv7a-linux-androideabi', + Target.androidArm64: 'aarch64-linux-android', + Target.androidIA32: 'i686-linux-android', + Target.androidX64: 'x86_64-linux-android', + }; + + static const appleClangMacosTargetFlags = { + Target.macOSArm64: 'arm64-apple-darwin', + Target.macOSX64: 'x86_64-apple-darwin', + }; + + static const appleClangIosTargetFlags = { + Target.iOSArm64: { + IOSSdk.iPhoneOs: 'arm64-apple-ios', + IOSSdk.iPhoneSimulator: 'arm64-apple-ios-simulator', + }, + Target.iOSX64: { + IOSSdk.iPhoneSimulator: 'x86_64-apple-ios-simulator', + }, + }; + + static const defaultCppLinkStdLib = { + OS.android: 'c++_shared', + OS.fuchsia: 'c++', + OS.iOS: 'c++', + OS.linux: 'stdc++', + OS.macOS: 'c++', + }; +}
diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart new file mode 100644 index 0000000..31edfc5 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart
@@ -0,0 +1,132 @@ +// 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. + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_instance.dart'; +import '../tool/tool_resolver.dart'; +import 'clang.dart'; + +final androidNdk = Tool( + name: 'Android NDK', + defaultResolver: _AndroidNdkResolver(), +); + +/// [clang] with [Tool.defaultResolver] for the [OS.android] NDK. +final androidNdkClang = Tool( + name: clang.name, + defaultResolver: _AndroidNdkResolver(), +); + +/// [llvmAr] with [Tool.defaultResolver] for the [OS.android] NDK. +final androidNdkLlvmAr = Tool( + name: llvmAr.name, + defaultResolver: _AndroidNdkResolver(), +); + +/// [lld] with [Tool.defaultResolver] for the [OS.android] NDK. +final androidNdkLld = Tool( + name: lld.name, + defaultResolver: _AndroidNdkResolver(), +); + +class _AndroidNdkResolver implements ToolResolver { + final installLocationResolver = PathVersionResolver( + wrappedResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'Android NDK', + wrappedResolver: PathToolResolver( + toolName: 'ndk-build', + executableName: Platform.isWindows ? 'ndk-build.cmd' : 'ndk-build', + ), + relativePath: Uri(path: ''), + ), + InstallLocationResolver( + toolName: 'Android NDK', + paths: [ + if (Platform.isLinux) ...[ + '\$HOME/Android/Sdk/ndk/*/', + '\$HOME/Android/Sdk/ndk-bundle/', + ], + if (Platform.isMacOS) ...[ + '\$HOME/Library/Android/sdk/ndk/*/', + ], + if (Platform.isWindows) ...[ + '\$HOME/AppData/Local/Android/Sdk/ndk/*/', + ], + ], + ), + ]), + ); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final ndkInstances = await installLocationResolver.resolve(logger: logger); + + return [ + for (final ndkInstance in ndkInstances) ...[ + ndkInstance, + ...await tryResolveClang( + ndkInstance, + logger: logger, + ) + ] + ]; + } + + Future<List<ToolInstance>> tryResolveClang( + ToolInstance androidNdkInstance, { + required Logger? logger, + }) async { + final result = <ToolInstance>[]; + final prebuiltUri = + androidNdkInstance.uri.resolve('toolchains/llvm/prebuilt/'); + final prebuiltDir = Directory.fromUri(prebuiltUri); + final hostArchDirs = + (await prebuiltDir.list().toList()).whereType<Directory>().toList(); + for (final hostArchDir in hostArchDirs) { + final clangUri = hostArchDir.uri + .resolve('bin/') + .resolve(Target.current.os.executableFileName('clang')); + if (await File.fromUri(clangUri).exists()) { + result.add(await CliVersionResolver.lookupVersion( + ToolInstance( + tool: androidNdkClang, + uri: clangUri, + ), + logger: logger, + )); + } + final arUri = hostArchDir.uri + .resolve('bin/') + .resolve(Target.current.os.executableFileName('llvm-ar')); + if (await File.fromUri(arUri).exists()) { + result.add(await CliVersionResolver.lookupVersion( + ToolInstance( + tool: androidNdkLlvmAr, + uri: arUri, + ), + logger: logger, + )); + } + final ldUri = hostArchDir.uri + .resolve('bin/') + .resolve(Target.current.os.executableFileName('ld.lld')); + if (await File.fromUri(arUri).exists()) { + result.add(await CliVersionResolver.lookupVersion( + ToolInstance( + tool: androidNdkLld, + uri: ldUri, + ), + logger: logger, + )); + } + } + return result; + } +}
diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/apple_clang.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/apple_clang.dart new file mode 100644 index 0000000..9c7eb4c --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/apple_clang.dart
@@ -0,0 +1,60 @@ +// 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. + +import '../tool/tool.dart'; +import '../tool/tool_resolver.dart'; + +/// The Clang variant inside XCode. +/// +/// https://developer.apple.com/xcode/ +final Tool appleClang = Tool( + name: 'Apple Clang', + defaultResolver: CliVersionResolver( + wrappedResolver: CliFilter( + cliArguments: ['--version'], + keepIf: ({required String stdout}) => stdout.contains('Apple clang'), + wrappedResolver: PathToolResolver( + toolName: 'Apple Clang', + executableName: 'clang', + ), + ), + ), +); + +/// The archiver belonging to [appleClang]. +final Tool appleAr = Tool( + name: 'Apple archiver', + defaultResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'Apple archiver', + wrappedResolver: appleClang.defaultResolver!, + relativePath: Uri.file('ar'), + ), + ]), +); + +/// The linker belonging to [appleClang]. +final Tool appleLd = Tool( + name: 'Apple linker', + defaultResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'Apple linker', + wrappedResolver: appleClang.defaultResolver!, + relativePath: Uri.file('ld'), + ), + ]), +); + +/// The Mach-O dumping tool. +/// +/// https://llvm.org/docs/CommandGuide/llvm-otool.html +final Tool otool = Tool( + name: 'otool', + defaultResolver: CliVersionResolver( + wrappedResolver: PathToolResolver( + toolName: 'otool', + executableName: 'otool', + ), + ), +);
diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/clang.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/clang.dart new file mode 100644 index 0000000..910e3b3 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/clang.dart
@@ -0,0 +1,55 @@ +// 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. + +import '../tool/tool.dart'; +import '../tool/tool_resolver.dart'; + +/// The Clang compiler. +/// +/// https://clang.llvm.org/ +final Tool clang = Tool( + name: 'Clang', + defaultResolver: CliVersionResolver( + wrappedResolver: CliFilter( + cliArguments: ['--version'], + keepIf: ({required String stdout}) => !stdout.contains('Apple clang'), + wrappedResolver: PathToolResolver( + toolName: 'Clang', + executableName: 'clang', + ), + ), + ), +); + +/// The LLVM archiver. +/// +/// https://llvm.org/docs/CommandGuide/llvm-ar.html +final Tool llvmAr = Tool( + name: 'LLVM archiver', + defaultResolver: CliVersionResolver( + wrappedResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'LLVM archiver', + wrappedResolver: clang.defaultResolver!, + relativePath: Uri.file('llvm-ar'), + ), + ]), + ), +); + +/// The LLVM Linker. +/// +/// https://lld.llvm.org/ +final Tool lld = Tool( + name: 'LLD', + defaultResolver: CliVersionResolver( + wrappedResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'LLD', + wrappedResolver: clang.defaultResolver!, + relativePath: Uri.file('ld.lld'), + ), + ]), + ), +);
diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/gcc.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/gcc.dart new file mode 100644 index 0000000..c3ea294 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/gcc.dart
@@ -0,0 +1,100 @@ +// 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. + +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_resolver.dart'; + +/// The GNU Compiler Collection for [Architecture.current]. +/// +/// https://gcc.gnu.org/ +final gcc = Tool(name: 'GCC'); + +/// The GNU GCC archiver for [Architecture.current]. +final gnuArchiver = Tool(name: 'GNU archiver'); + +/// The GNU linker for [Architecture.current]. +/// +/// https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/ld.html +final gnuLinker = Tool(name: 'GNU linker'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.ia32]. +final i686LinuxGnuGcc = _gcc('i686-linux-gnu'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.ia32]. +final i686LinuxGnuGccAr = _gnuArchiver('i686-linux-gnu'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.ia32]. +final i686LinuxGnuLd = _gnuLinker('i686-linux-gnu'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.x64]. +final x86_64LinuxGnuGcc = _gcc('x86_64-linux-gnu'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.x64]. +final x86_64LinuxGnuGccAr = _gnuArchiver('x86_64-linux-gnu'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.x64]. +final x86_64LinuxGnuLd = _gnuLinker('x86_64-linux-gnu'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.arm]. +final armLinuxGnueabihfGcc = _gcc('arm-linux-gnueabihf'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.arm]. +final armLinuxGnueabihfGccAr = _gnuArchiver('arm-linux-gnueabihf'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.arm]. +final armLinuxGnueabihfLd = _gnuLinker('arm-linux-gnueabihf'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.arm64]. +final aarch64LinuxGnuGcc = _gcc('aarch64-linux-gnu'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.arm64]. +final aarch64LinuxGnuGccAr = _gnuArchiver('aarch64-linux-gnu'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.arm64]. +final aarch64LinuxGnuLd = _gnuLinker('aarch64-linux-gnu'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.riscv64]. +final riscv64LinuxGnuGcc = _gcc('riscv64-linux-gnu'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.riscv64]. +final riscv64LinuxGnuGccAr = _gnuArchiver('riscv64-linux-gnu'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.riscv64]. +final riscv64LinuxGnuLd = _gnuLinker('riscv64-linux-gnu'); + +Tool _gcc(String prefix) => Tool( + name: gcc.name, + defaultResolver: CliVersionResolver( + wrappedResolver: PathToolResolver( + toolName: gcc.name, + executableName: '$prefix-gcc', + ), + ), + ); + +Tool _gnuArchiver(String prefix) { + final gcc = _gcc(prefix); + return Tool( + name: gnuArchiver.name, + defaultResolver: RelativeToolResolver( + toolName: gnuArchiver.name, + wrappedResolver: gcc.defaultResolver!, + relativePath: Uri.file('$prefix-gcc-ar'), + ), + ); +} + +Tool _gnuLinker(String prefix) { + final gcc = _gcc(prefix); + return Tool( + name: gnuLinker.name, + defaultResolver: RelativeToolResolver( + toolName: gnuLinker.name, + wrappedResolver: gcc.defaultResolver!, + relativePath: Uri.file('$prefix-ld'), + ), + ); +}
diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart new file mode 100644 index 0000000..a95356e --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart
@@ -0,0 +1,310 @@ +// 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. + +import 'dart:io'; + +import 'package:glob/glob.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_instance.dart'; +import '../tool/tool_resolver.dart'; +import '../utils/run_process.dart'; +import '../utils/sem_version.dart'; + +/// The Visual Studio Locator. +/// +/// https://github.com/microsoft/vswhere +final Tool vswhere = Tool( + name: 'Visual Studio Locator', + defaultResolver: CliVersionResolver( + arguments: [], + wrappedResolver: ToolResolvers( + [ + PathToolResolver( + toolName: 'Visual Studio Locator', + executableName: 'vswhere.exe', + ), + InstallLocationResolver( + toolName: 'Visual Studio Locator', + paths: [ + 'C:/Program Files \\(x86\\)/Microsoft Visual Studio/Installer/vswhere.exe', + 'C:/Program Files/Microsoft Visual Studio/Installer/vswhere.exe', + ], + ), + ], + ), + ), +); + +/// Visual Studio. +/// +/// https://visualstudio.microsoft.com/ +final Tool visualStudio = Tool( + name: 'Visual Studio', + defaultResolver: VisualStudioResolver(), +); + +/// The C/C++ Optimizing Compiler. +final Tool msvc = Tool( + name: 'MSVC', + defaultResolver: PathVersionResolver( + wrappedResolver: RelativeToolResolver( + toolName: 'MSVC', + wrappedResolver: visualStudio.defaultResolver!, + relativePath: Uri(path: './VC/Tools/MSVC/*/'), + ), + ), +); + +Tool vcvars(ToolInstance toolInstance) { + final tool = toolInstance.tool; + assert(tool == cl || tool == link || tool == lib); + final vcDir = toolInstance.uri.resolve('../../../../../../'); + final String fileName; + if (toolInstance.uri.toFilePath().contains('\\x86\\')) { + fileName = 'vcvars32.bat'; + } else if (toolInstance.uri.toFilePath().contains('\\arm64\\')) { + // TODO(https://github.com/dart-lang/native/issues/170): Support native + // windows-arm64 MSVC toolchain. + // vcvarsarm64 only works on native windows-arm64. In case of cross + // compilation, it's better to stick to cross toolchain, which works under + // emulation on windows-arm64. + fileName = 'vcvarsamd64_arm64.bat'; + } else { + fileName = 'vcvars64.bat'; + } + final batchScript = vcDir.resolve('Auxiliary/Build/$fileName'); + return Tool( + name: fileName, + defaultResolver: InstallLocationResolver( + toolName: fileName, + paths: [ + Glob.quote(batchScript.toFilePath().replaceAll('\\', '/')), + ], + ), + ); +} + +final Tool vcvars64 = Tool( + name: 'vcvars64.bat', + defaultResolver: RelativeToolResolver( + toolName: 'vcvars64.bat', + wrappedResolver: visualStudio.defaultResolver!, + relativePath: Uri(path: './VC/Auxiliary/Build/vcvars64.bat'), + ), +); + +final Tool vcvars32 = Tool( + name: 'vcvars32.bat', + defaultResolver: RelativeToolResolver( + toolName: 'vcvars32.bat', + wrappedResolver: visualStudio.defaultResolver!, + relativePath: Uri(path: './VC/Auxiliary/Build/vcvars32.bat'), + ), +); + +final Tool vcvarsarm64 = Tool( + // TODO(https://github.com/dart-lang/native/issues/170): Support native + // windows-arm64 MSVC toolchain. + // vcvarsarm64 only works on native windows-arm64. In case of cross + // compilation, it's better to stick to cross toolchain, which works under + // emulation on windows-arm64. + name: 'vcvarsamd64_arm64.bat', + defaultResolver: RelativeToolResolver( + toolName: 'vcvarsamd64_arm64.bat', + wrappedResolver: visualStudio.defaultResolver!, + relativePath: Uri(path: './VC/Auxiliary/Build/vcvarsamd64_arm64.bat'), + ), +); + +final Tool vcvarsall = Tool( + name: 'vcvarsall.bat', + defaultResolver: RelativeToolResolver( + toolName: 'vcvars32.bat', + wrappedResolver: visualStudio.defaultResolver!, + relativePath: Uri(path: './VC/Auxiliary/Build/vcvarsall.bat'), + ), +); + +final Tool vsDevCmd = Tool( + name: 'VsDevCmd.bat', + defaultResolver: RelativeToolResolver( + toolName: 'VsDevCmd.bat', + wrappedResolver: visualStudio.defaultResolver!, + relativePath: Uri(path: './Common7/Tools/VsDevCmd.bat'), + ), +); + +/// The C/C++ Optimizing Compiler main executable. +/// +/// For targeting [Architecture.x64]. +final Tool cl = _msvcTool( + name: 'cl', + versionArguments: [], + targetArchitecture: Architecture.x64, + hostArchitecture: Target.current.architecture, +); + +/// The C/C++ Optimizing Compiler main executable. +/// +/// For targeting [Architecture.ia32]. +final Tool clIA32 = _msvcTool( + name: 'cl', + versionArguments: [], + targetArchitecture: Architecture.ia32, + hostArchitecture: Target.current.architecture, +); + +/// The C/C++ Optimizing Compiler main executable. +/// +/// For targeting [Architecture.arm64]. +final Tool clArm64 = _msvcTool( + name: 'cl', + versionArguments: [], + targetArchitecture: Architecture.arm64, + hostArchitecture: Target.current.architecture, +); + +final Tool lib = _msvcTool( + name: 'lib', + targetArchitecture: Architecture.x64, + hostArchitecture: Target.current.architecture, + // https://github.com/dart-lang/native/issues/18 + resolveVersion: false, +); + +final Tool libIA32 = _msvcTool( + name: 'lib', + targetArchitecture: Architecture.ia32, + hostArchitecture: Target.current.architecture, + // https://github.com/dart-lang/native/issues/18 + resolveVersion: false, +); + +final Tool libArm64 = _msvcTool( + name: 'lib', + targetArchitecture: Architecture.arm64, + hostArchitecture: Target.current.architecture, + // https://github.com/dart-lang/native/issues/18 + resolveVersion: false, +); + +final Tool link = _msvcTool( + name: 'link', + versionArguments: ['/help'], + versionExitCode: 1100, + targetArchitecture: Architecture.x64, + hostArchitecture: Target.current.architecture, +); + +final Tool linkIA32 = _msvcTool( + name: 'link', + versionArguments: ['/help'], + versionExitCode: 1100, + targetArchitecture: Architecture.ia32, + hostArchitecture: Target.current.architecture, +); + +final Tool linkArm64 = _msvcTool( + name: 'link', + versionArguments: ['/help'], + versionExitCode: 1100, + targetArchitecture: Architecture.arm64, + hostArchitecture: Target.current.architecture, +); + +final Tool dumpbin = _msvcTool( + name: 'dumpbin', + targetArchitecture: Architecture.x64, + hostArchitecture: Target.current.architecture, +); + +const _msvcArchNames = { + Architecture.ia32: 'x86', + Architecture.x64: 'x64', + Architecture.arm64: 'arm64', +}; + +Tool _msvcTool({ + required String name, + required Architecture targetArchitecture, + required Architecture hostArchitecture, + List<String> versionArguments = const ['--version'], + int versionExitCode = 0, + bool resolveVersion = true, +}) { + final executableName = OS.windows.executableFileName(name); + if (Target.current.os != OS.windows) { + return Tool( + name: executableName, + defaultResolver: ToolResolvers([]), + ); + } + final hostArchName = _msvcArchNames[hostArchitecture]!; + final targetArchName = _msvcArchNames[targetArchitecture]!; + ToolResolver resolver = RelativeToolResolver( + toolName: executableName, + wrappedResolver: msvc.defaultResolver!, + relativePath: Uri( + path: 'bin/Host$hostArchName/$targetArchName/$executableName', + ), + ); + if (resolveVersion) { + resolver = CliVersionResolver( + expectedExitCode: versionExitCode, + arguments: versionArguments, + wrappedResolver: resolver, + ); + } + return Tool( + name: executableName, + defaultResolver: resolver, + ); +} + +class VisualStudioResolver implements ToolResolver { + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final vswhereInstances = + await vswhere.defaultResolver!.resolve(logger: logger); + + final result = <ToolInstance>[]; + for (final vswhereInstance in vswhereInstances.take(1)) { + final vswhereResult = await runProcess( + executable: vswhereInstance.uri, + logger: logger, + ); + final toolInfos = vswhereResult.stdout.split(_newLine * 2).skip(1); + for (final toolInfo in toolInfos) { + final toolInfoParsed = parseToolInfo(toolInfo); + final dir = Directory(toolInfoParsed['installationPath']!); + assert(await dir.exists()); + final uri = dir.uri; + final version = versionFromString(toolInfoParsed['installationName']!); + final instance = + ToolInstance(tool: visualStudio, uri: uri, version: version); + logger?.fine('Found $instance.'); + result.add(instance); + } + } + return result; + } + + static Map<String, String> parseToolInfo(String toolInfo) { + final result = <String, String>{}; + final lines = toolInfo.split(_newLine); + for (final line in lines) { + final splitLine = line.split(': '); + final key = splitLine.first; + final value = splitLine.skip(1).join(': '); + result[key] = value; + } + return result; + } +} + +// runProcess uses writeln which uses '\n'. +const _newLine = '\n';
diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/recognizer.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/recognizer.dart new file mode 100644 index 0000000..0de8418 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/recognizer.dart
@@ -0,0 +1,148 @@ +// 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. + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_instance.dart'; +import '../tool/tool_resolver.dart'; +import 'apple_clang.dart'; +import 'clang.dart'; +import 'gcc.dart'; +import 'msvc.dart'; + +class CompilerRecognizer implements ToolResolver { + final Uri uri; + + CompilerRecognizer(this.uri); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final os = Target.current.os; + logger?.finer('Trying to recognize $uri.'); + final filePath = uri.toFilePath(); + Tool? tool; + if (filePath.contains('-gcc')) { + tool = gcc; + } else if (filePath.endsWith(os.executableFileName('clang'))) { + final stdout = await CliFilter.executeCli(uri, + arguments: ['--version'], logger: logger); + if (stdout.contains('Apple clang')) { + tool = appleClang; + } else { + tool = clang; + } + } else if (filePath.endsWith('cl.exe')) { + tool = cl; + } + + if (tool != null) { + logger?.fine('Tool instance $uri is likely $tool.'); + final toolInstance = ToolInstance(tool: tool, uri: uri); + return [ + await CliVersionResolver.lookupVersion( + toolInstance, + logger: logger, + arguments: [ + if (tool != cl) '--version', + ], + ), + ]; + } + + logger?.severe('Tool instance $uri not recognized.'); + return []; + } +} + +class LinkerRecognizer implements ToolResolver { + final Uri uri; + + LinkerRecognizer(this.uri); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final os = Target.current.os; + logger?.finer('Trying to recognize $uri.'); + final filePath = uri.toFilePath(); + Tool? tool; + if (filePath.contains('-ld')) { + tool = gnuLinker; + } else if (filePath.endsWith(os.executableFileName('ld.lld'))) { + tool = lld; + } else if (filePath.endsWith(os.executableFileName('ld'))) { + tool = appleLd; + } else if (filePath.endsWith('link.exe')) { + tool = link; + } + + if (tool != null) { + logger?.fine('Tool instance $uri is likely $tool.'); + final toolInstance = ToolInstance(tool: tool, uri: uri); + if (tool == lld) { + return [ + await CliVersionResolver.lookupVersion( + toolInstance, + logger: logger, + ), + ]; + } + if (tool == link) { + return [ + await CliVersionResolver.lookupVersion( + toolInstance, + logger: logger, + arguments: ['/help'], + expectedExitCode: 1100, + ), + ]; + } + return [toolInstance]; + } + + logger?.severe('Tool instance $uri not recognized.'); + return []; + } +} + +class ArchiverRecognizer implements ToolResolver { + final Uri uri; + + ArchiverRecognizer(this.uri); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + logger?.finer('Trying to recognize $uri.'); + final os = Target.current.os; + final filePath = uri.toFilePath(); + Tool? tool; + if (filePath.contains('-gcc-ar')) { + tool = gnuArchiver; + } else if (filePath.endsWith(os.executableFileName('llvm-ar'))) { + tool = llvmAr; + } else if (filePath.endsWith(os.executableFileName('ar'))) { + tool = appleAr; + } else if (filePath.endsWith('lib.exe')) { + tool = lib; + } + + if (tool != null) { + logger?.fine('Tool instance $uri is likely $tool.'); + final toolInstance = ToolInstance(tool: tool, uri: uri); + if (tool == llvmAr) { + return [ + await CliVersionResolver.lookupVersion( + toolInstance, + logger: logger, + ), + ]; + } + return [toolInstance]; + } + + logger?.severe('Tool instance $uri not recognized.'); + return []; + } +}
diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart new file mode 100644 index 0000000..48c25c3 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart
@@ -0,0 +1,102 @@ +// 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. + +import 'dart:io'; + +import 'package:logging/logging.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_instance.dart'; +import '../tool/tool_resolver.dart'; +import '../utils/run_process.dart'; + +/// The xcrun from XCode. +/// +/// https://developer.apple.com/xcode/ +final Tool xcrun = Tool( + name: 'xcrun', + defaultResolver: CliVersionResolver( + wrappedResolver: PathToolResolver( + toolName: 'xcrun', + executableName: 'xcrun', + ), + ), +); + +/// The MacOSX SDK. +final Tool macosxSdk = Tool( + name: 'MacOSX SDK', + defaultResolver: XCodeSdkResolver(), +); + +/// The iPhoneOS SDK. +final Tool iPhoneOSSdk = Tool( + name: 'iPhoneOS SDK', + defaultResolver: XCodeSdkResolver(), +); + +/// The iPhoneSimulator SDK. +final Tool iPhoneSimulatorSdk = Tool( + name: 'iPhoneSimulator SDK', + defaultResolver: XCodeSdkResolver(), +); + +class XCodeSdkResolver implements ToolResolver { + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final xcrunInstances = await xcrun.defaultResolver!.resolve(logger: logger); + + return [ + for (final xcrunInstance in xcrunInstances) ...[ + ...await tryResolveSdk( + xcrunInstance: xcrunInstance, + sdk: 'macosx', + tool: macosxSdk, + logger: logger, + ), + ...await tryResolveSdk( + xcrunInstance: xcrunInstance, + sdk: 'iphoneos', + tool: iPhoneOSSdk, + logger: logger, + ), + ...await tryResolveSdk( + xcrunInstance: xcrunInstance, + sdk: 'iphonesimulator', + tool: iPhoneSimulatorSdk, + logger: logger, + ), + ], + // xcrun --sdk macosx --show-sdk-path) + ]; + } + + static Future<List<ToolInstance>> tryResolveSdk({ + required ToolInstance xcrunInstance, + required String sdk, + required Tool tool, + required Logger? logger, + }) async { + final result = await runProcess( + executable: xcrunInstance.uri, + arguments: ['--sdk', sdk, '--show-sdk-path'], + logger: logger, + ); + if (result.exitCode == 1) { + assert(result.stderr.contains('cannot be located')); + logger?.warning('SDK $sdk not installed.'); + return []; + } + assert(result.exitCode == 0); + final uriSymbolic = Uri.directory(result.stdout.trim()); + logger?.fine('Found $sdk at ${uriSymbolic.toFilePath()}'); + final uri = Uri.directory( + await Directory.fromUri(uriSymbolic).resolveSymbolicLinks()); + if (uriSymbolic != uri) { + logger?.fine('Found $sdk at ${uri.toFilePath()}'); + } + assert(await Directory.fromUri(uri).exists()); + return [ToolInstance(tool: tool, uri: uri)]; + } +}
diff --git a/pkgs/native_toolchain_c/lib/src/tool/tool.dart b/pkgs/native_toolchain_c/lib/src/tool/tool.dart new file mode 100644 index 0000000..ff42f28 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/tool/tool.dart
@@ -0,0 +1,25 @@ +// 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. + +import 'tool_resolver.dart'; + +class Tool { + final String name; + + ToolResolver? defaultResolver; + + Tool({ + required this.name, + this.defaultResolver, + }); + + @override + bool operator ==(Object other) => other is Tool && name == other.name; + + @override + int get hashCode => Object.hash(name, 133709); + + @override + String toString() => 'Tool($name)'; +}
diff --git a/pkgs/native_toolchain_c/lib/src/tool/tool_error.dart b/pkgs/native_toolchain_c/lib/src/tool/tool_error.dart new file mode 100644 index 0000000..def9ce4 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/tool/tool_error.dart
@@ -0,0 +1,14 @@ +// 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. + +/// The operation could not be performed due to a configuration error on the +/// host system. +class ToolError extends Error { + final String message; + + ToolError(this.message); + + @override + String toString() => 'System not configured correctly: $message'; +}
diff --git a/pkgs/native_toolchain_c/lib/src/tool/tool_instance.dart b/pkgs/native_toolchain_c/lib/src/tool/tool_instance.dart new file mode 100644 index 0000000..c1a373b --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/tool/tool_instance.dart
@@ -0,0 +1,78 @@ +// 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. + +import 'package:pub_semver/pub_semver.dart'; + +import 'tool.dart'; + +class ToolInstance implements Comparable<ToolInstance> { + /// The name of the tool. + final Tool tool; + + /// The path of the native tool on the system. + final Uri uri; + + /// The version of the native tool. + /// + /// Can be null if version is hard to determine. + final Version? version; + + ToolInstance({ + required this.tool, + required this.uri, + this.version, + }); + + ToolInstance copyWith({ + Uri? uri, + Version? version, + }) => + ToolInstance( + tool: tool, + uri: uri ?? this.uri, + version: version ?? this.version, + ); + + @override + String toString() => 'ToolInstance(${tool.name}, $version, $uri)'; + + /// Compares this tool instance to [other]. + /// + /// When used in sorting, orders [ToolInstance]s according to: + /// 1. [tool] name, alphabetically; then + /// 2. [version], newest first and preferring having a version; then + /// 3. [uri], alphabetically. + @override + int compareTo(ToolInstance other) { + final nameCompare = tool.name.compareTo(other.tool.name); + if (nameCompare != 0) { + return nameCompare; + } + final version = this.version; + final otherVersion = other.version; + if (version != null || otherVersion != null) { + if (version == null) { + return 1; + } + if (otherVersion == null) { + return -1; + } + final versionCompare = version.compareTo(otherVersion); + if (versionCompare != 0) { + return -versionCompare; + } + } + return uri.toFilePath().compareTo(other.uri.toFilePath()); + } + + @override + bool operator ==(Object other) => + other is ToolInstance && + tool == other.tool && + uri == other.uri && + version == other.version; + + @override + int get hashCode => Object.hash(tool, uri, version); +}
diff --git a/pkgs/native_toolchain_c/lib/src/tool/tool_requirement.dart b/pkgs/native_toolchain_c/lib/src/tool/tool_requirement.dart new file mode 100644 index 0000000..33dba8b --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/tool/tool_requirement.dart
@@ -0,0 +1,92 @@ +// 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. + +import 'package:pub_semver/pub_semver.dart'; + +import 'tool.dart'; +import 'tool_instance.dart'; + +abstract class Requirement { + /// Tries to satisfy this requirement. + /// + /// If the requirement can be satisfied, returns the set of tools that + /// satisfy the requirement. + /// + /// Currently does not check that we only use a single version of a tool. + List<ToolInstance>? satisfy(List<ToolInstance> allAvailableTools); +} + +class ToolRequirement implements Requirement { + final Tool tool; + + final Version? minimumVersion; + + ToolRequirement( + this.tool, { + this.minimumVersion, + }); + + @override + String toString() => + 'ToolRequirement(${tool.name}, minimumVersion: $minimumVersion)'; + + @override + List<ToolInstance>? satisfy(List<ToolInstance> availableToolInstances) { + final candidates = <ToolInstance>[]; + for (final instance in availableToolInstances) { + if (instance.tool == tool) { + final minimumVersion_ = minimumVersion; + if (minimumVersion_ == null) { + candidates.add(instance); + } else { + final version = instance.version; + if (version != null && version >= minimumVersion_) { + candidates.add(instance); + } + } + } + } + if (candidates.isEmpty) { + return null; + } + candidates.sort(); + return [candidates.first]; + } +} + +class RequireOne implements Requirement { + final List<Requirement> alternatives; + + RequireOne(this.alternatives); + + @override + List<ToolInstance>? satisfy(List<ToolInstance> allAvailableTools) { + for (final alternative in alternatives) { + final result = alternative.satisfy(allAvailableTools); + if (result != null) { + return result; + } + } + return null; + } +} + +class RequireAll implements Requirement { + final List<Requirement> requirements; + + RequireAll(this.requirements); + + @override + List<ToolInstance>? satisfy(List<ToolInstance> allAvailableTools) { + final result = <ToolInstance>[]; + for (final requirement in requirements) { + final requirementResult = requirement.satisfy(allAvailableTools); + if (requirementResult == null) { + return null; + } + result.addAll(requirementResult); + } + return result; + } +}
diff --git a/pkgs/native_toolchain_c/lib/src/tool/tool_resolver.dart b/pkgs/native_toolchain_c/lib/src/tool/tool_resolver.dart new file mode 100644 index 0000000..257ef8e --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/tool/tool_resolver.dart
@@ -0,0 +1,349 @@ +// 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. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:glob/glob.dart'; +import 'package:glob/list_local_fs.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import '../utils/run_process.dart'; +import '../utils/sem_version.dart'; +import 'tool.dart'; +import 'tool_error.dart'; +import 'tool_instance.dart'; + +abstract class ToolResolver { + /// Resolves tools on the host system. + Future<List<ToolInstance>> resolve({required Logger? logger}); +} + +/// Tries to resolve a tool on the `PATH`. +/// +/// Uses `which` (`where` on Windows) to resolve a tool. +class PathToolResolver extends ToolResolver { + /// The [Tool.name] of the [Tool] to find on the `PATH`. + final String toolName; + + final String executableName; + + PathToolResolver({ + required this.toolName, + String? executableName, + }) : executableName = executableName ?? + Target.current.os.executableFileName(toolName.toLowerCase()); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + logger?.finer('Looking for $toolName on PATH.'); + final uri = await runWhich(logger: logger); + if (uri == null) { + logger?.fine('Did not find $toolName on PATH.'); + return []; + } + final toolInstances = [ + ToolInstance(tool: Tool(name: toolName), uri: uri), + ]; + logger?.fine('Found ${toolInstances.single}.'); + return toolInstances; + } + + static Uri get which => Uri.file(Platform.isWindows ? 'where' : 'which'); + + Future<Uri?> runWhich({required Logger? logger}) async { + final process = await runProcess( + executable: which, + arguments: [executableName], + logger: logger, + ); + if (process.exitCode == 0) { + final file = File(LineSplitter.split(process.stdout).first); + final uri = File(await file.resolveSymbolicLinks()).uri; + if (uri.pathSegments.last == 'llvm') { + // https://github.com/dart-lang/native/issues/136 + return file.uri; + } + return uri; + } + // The exit code for executable not being on the `PATH`. + assert(process.exitCode == 1); + return null; + } +} + +class CliVersionResolver implements ToolResolver { + final ToolResolver wrappedResolver; + final List<String> arguments; + final int expectedExitCode; + + CliVersionResolver({ + required this.wrappedResolver, + this.arguments = const ['--version'], + this.expectedExitCode = 0, + }); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final toolInstances = await wrappedResolver.resolve(logger: logger); + return [ + for (final toolInstance in toolInstances) + await lookupVersion( + toolInstance, + arguments: arguments, + expectedExitCode: expectedExitCode, + logger: logger, + ) + ]; + } + + static Future<ToolInstance> lookupVersion( + ToolInstance toolInstance, { + List<String> arguments = const ['--version'], + int expectedExitCode = 0, + required Logger? logger, + }) async { + if (toolInstance.version != null) return toolInstance; + logger?.finer('Looking up version with --version for $toolInstance.'); + final version = await executableVersion( + toolInstance.uri, + arguments: arguments, + expectedExitCode: expectedExitCode, + logger: logger, + ); + final result = toolInstance.copyWith(version: version); + logger?.fine('Found version for $result.'); + return result; + } + + static Future<Version> executableVersion( + Uri executable, { + List<String> arguments = const ['--version'], + int expectedExitCode = 0, + required Logger? logger, + }) async { + final process = await runProcess( + executable: executable, + arguments: arguments, + logger: logger, + ); + if (process.exitCode != expectedExitCode) { + final executablePath = executable.toFilePath(); + throw ToolError( + '`$executablePath ${arguments.join(' ')}` returned unexpected exit' + ' code: ${process.exitCode}.'); + } + return versionFromString(process.stderr) ?? + versionFromString(process.stdout)!; + } +} + +class PathVersionResolver implements ToolResolver { + ToolResolver wrappedResolver; + + PathVersionResolver({required this.wrappedResolver}); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final toolInstances = await wrappedResolver.resolve(logger: logger); + + return [ + for (final toolInstance in toolInstances) lookupVersion(toolInstance) + ]; + } + + static ToolInstance lookupVersion(ToolInstance toolInstance) { + if (toolInstance.version != null) { + return toolInstance; + } + return toolInstance.copyWith( + version: version(toolInstance.uri), + ); + } + + static Version? version(Uri uri) { + final versionString = uri.pathSegments.where((e) => e != '').last; + final version = versionFromString(versionString); + return version; + } +} + +/// A resolver which invokes all [resolvers] tools. +class ToolResolvers implements ToolResolver { + final List<ToolResolver> resolvers; + + ToolResolvers(this.resolvers); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async => [ + for (final resolver in resolvers) + ...await resolver.resolve(logger: logger) + ]; +} + +class InstallLocationResolver implements ToolResolver { + final String toolName; + final List<String> paths; + + InstallLocationResolver({ + required this.toolName, + required this.paths, + }); + + static const home = '\$HOME'; + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + logger?.finer('Looking for $toolName in $paths.'); + final resolvedPaths = [ + for (final path in paths) ...await tryResolvePath(path) + ]; + final toolInstances = [ + for (final uri in resolvedPaths) + ToolInstance(tool: Tool(name: toolName), uri: uri), + ]; + if (toolInstances.isNotEmpty) { + logger?.fine('Found $toolInstances.'); + } else { + logger?.finer('Found no $toolName in $paths.'); + } + return toolInstances; + } + + Future<List<Uri>> tryResolvePath(String path) async { + if (path.startsWith(home)) { + final homeDir_ = homeDir; + assert(homeDir_ != null); + path = path.replaceAll( + '$home/', homeDir!.toFilePath().replaceAll('\\', '/')); + } + + final result = <Uri>[]; + final fileSystemEntities = await Glob(path).list().toList(); + for (final fileSystemEntity in fileSystemEntities) { + if (!await fileSystemEntity.exists()) { + continue; + } + if (fileSystemEntity is! Directory && path.endsWith('/')) { + continue; + } + result.add(fileSystemEntity.uri); + } + return result; + } + + static final Uri? homeDir = () { + final path = + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + if (path == null) return null; + return Directory(path).uri; + }(); +} + +class RelativeToolResolver implements ToolResolver { + final String toolName; + final ToolResolver wrappedResolver; + final Uri relativePath; + + RelativeToolResolver({ + required this.toolName, + required this.wrappedResolver, + required this.relativePath, + }); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final otherToolInstances = await wrappedResolver.resolve(logger: logger); + + logger?.finer('Looking for $toolName relative to $otherToolInstances ' + 'with $relativePath.'); + final globs = [ + for (final toolInstance in otherToolInstances) + Glob([ + Glob.quote( + toolInstance.uri.resolve('.').toFilePath().replaceAll('\\', '/')), + relativePath.path + ].join()) + ]; + // print(globs); + // exit(0); + final fileSystemEntities = [ + for (final glob in globs) ...await glob.list().toList(), + ]; + + final result = [ + for (final fileSystemEntity in fileSystemEntities) + ToolInstance( + tool: Tool(name: toolName), + uri: fileSystemEntity.uri, + ), + ]; + + if (result.isNotEmpty) { + logger?.fine('Found $result.'); + } else { + logger?.finer('Found no $toolName relative to $otherToolInstances.'); + } + return result; + } +} + +class CliFilter implements ToolResolver { + final ToolResolver wrappedResolver; + final List<String> cliArguments; + final bool Function({required String stdout}) keepIf; + + CliFilter({ + required this.wrappedResolver, + required this.cliArguments, + required this.keepIf, + }); + + @override + Future<List<ToolInstance>> resolve({required Logger? logger}) async { + final toolInstances = await wrappedResolver.resolve(logger: logger); + return [ + for (final toolInstance in toolInstances) + await filter(toolInstance, logger: logger) + ].whereType<ToolInstance>().toList(); + } + + Future<ToolInstance?> filter( + ToolInstance toolInstance, { + required Logger? logger, + }) async { + if (toolInstance.version != null) return toolInstance; + logger?.finer('Checking if $toolInstance satisfies CLI filter.'); + final stdout = await executeCli( + toolInstance.uri, + arguments: cliArguments, + logger: logger, + ); + final doKeep = keepIf(stdout: stdout); + if (doKeep) { + logger?.fine('$toolInstance satisfies CLI filter.'); + return toolInstance; + } + logger?.fine('$toolInstance does not satisfy CLI filter.'); + return null; + } + + static Future<String> executeCli( + Uri executable, { + required List<String> arguments, + int expectedExitCode = 0, + required Logger? logger, + }) async { + final process = await runProcess( + executable: executable, + arguments: arguments, + logger: logger, + ); + final exitCode = process.exitCode; + assert(exitCode == expectedExitCode); + return process.stdout; + } +}
diff --git a/pkgs/native_toolchain_c/lib/src/utils/env_from_bat.dart b/pkgs/native_toolchain_c/lib/src/utils/env_from_bat.dart new file mode 100644 index 0000000..31958bb --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/utils/env_from_bat.dart
@@ -0,0 +1,48 @@ +// 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. + +import 'dart:io'; + +Future<Map<String, String>> envFromBat( + Uri batchFile, { + List<String> arguments = const [], +}) async { + final fileName = batchFile.pathSegments.last; + final dir = batchFile.resolve('.'); + const separator = '======='; + final processResult = await Process.run( + 'set && echo $separator && $fileName ${arguments.join(' ')} > nul && set', + [], + runInShell: true, + workingDirectory: dir.toFilePath(), + ); + assert(processResult.exitCode == 0); + final resultSplit = (processResult.stdout as String).split(separator); + assert(resultSplit.length == 2); + final unmodifiedParsed = parseDefines(resultSplit.first.trim()); + final modifiedParsed = parseDefines(resultSplit[1].trim()); + final result = <String, String>{}; + for (final entry in modifiedParsed.entries) { + final key = entry.key; + final value = entry.value; + if (!unmodifiedParsed.containsKey(key) || value != unmodifiedParsed[key]) { + result[key] = value; + } + } + return result; +} + +// Ensures it doesn't return empty keys. +Map<String, String> parseDefines(String defines) { + final result = <String, String>{}; + final lines = defines.trim().split('\r\n'); + for (final line in lines) { + if (line.isEmpty) continue; + final lineSplit = line.split('='); + final key = lineSplit.first; + final value = lineSplit.skip(1).join('='); + result[key] = value; + } + return result; +}
diff --git a/pkgs/native_toolchain_c/lib/src/utils/run_process.dart b/pkgs/native_toolchain_c/lib/src/utils/run_process.dart new file mode 100644 index 0000000..f9204e5 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/utils/run_process.dart
@@ -0,0 +1,130 @@ +// 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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +/// Runs a [Process]. +/// +/// If [logger] is provided, stream stdout and stderr to it. +/// +/// If [captureOutput], captures stdout and stderr. +Future<RunProcessResult> runProcess({ + required Uri executable, + List<String> arguments = const [], + Uri? workingDirectory, + Map<String, String>? environment, + bool includeParentEnvironment = true, + required Logger? logger, + bool captureOutput = true, + int expectedExitCode = 0, + bool throwOnUnexpectedExitCode = false, +}) async { + if (Platform.isWindows && !includeParentEnvironment) { + const winEnvKeys = [ + 'SYSTEMROOT', + 'TEMP', + 'TMP', + ]; + environment = { + for (final winEnvKey in winEnvKeys) + winEnvKey: Platform.environment[winEnvKey]!, + ...?environment, + }; + } + + final printWorkingDir = + workingDirectory != null && workingDirectory != Directory.current.uri; + final commandString = [ + if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};', + ...?environment?.entries.map((entry) => '${entry.key}=${entry.value}'), + executable.toFilePath(), + ...arguments.map((a) => a.contains(' ') ? "'$a'" : a), + if (printWorkingDir) ')', + ].join(' '); + logger?.info('Running `$commandString`.'); + + final stdoutBuffer = StringBuffer(); + final stderrBuffer = StringBuffer(); + final process = await Process.start( + executable.toFilePath(), + arguments, + workingDirectory: workingDirectory?.toFilePath(), + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: Platform.isWindows && !includeParentEnvironment, + ); + + final stdoutSub = process.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(captureOutput + ? (s) { + logger?.fine(s); + stdoutBuffer.writeln(s); + } + : logger?.fine); + final stderrSub = process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(captureOutput + ? (s) { + logger?.severe(s); + stderrBuffer.writeln(s); + } + : logger?.severe); + + final (exitCode, _, _) = await ( + process.exitCode, + stdoutSub.asFuture<void>(), + stderrSub.asFuture<void>() + ).wait; + final result = RunProcessResult( + pid: process.pid, + command: commandString, + exitCode: exitCode, + stdout: stdoutBuffer.toString(), + stderr: stderrBuffer.toString(), + ); + if (throwOnUnexpectedExitCode && expectedExitCode != exitCode) { + throw ProcessException( + executable.toFilePath(), + arguments, + "Full command string: '$commandString'.\n" + "Exit code: '$exitCode'.\n" + 'For the output of the process check the logger output.', + ); + } + return result; +} + +/// Drop in replacement of [ProcessResult]. +class RunProcessResult { + final int pid; + + final String command; + + final int exitCode; + + final String stderr; + + final String stdout; + + RunProcessResult({ + required this.pid, + required this.command, + required this.exitCode, + required this.stderr, + required this.stdout, + }); + + @override + String toString() => '''command: $command +exitCode: $exitCode +stdout: $stdout +stderr: $stderr'''; +}
diff --git a/pkgs/native_toolchain_c/lib/src/utils/sem_version.dart b/pkgs/native_toolchain_c/lib/src/utils/sem_version.dart new file mode 100644 index 0000000..39f8342 --- /dev/null +++ b/pkgs/native_toolchain_c/lib/src/utils/sem_version.dart
@@ -0,0 +1,42 @@ +// 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. + +import 'package:pub_semver/pub_semver.dart'; + +/// Reads a version out of a string. +/// +/// If no semantic version is found, tries to read find a version number +/// that doesn't follow semantic versioning. It will default [Version.minor] +/// and [Version.patch] to `0` if the relaxed version doesn't contain them. +Version? versionFromString(String containsVersion) { + final match = _semverRegex.firstMatch(containsVersion); + if (match != null) { + return Version( + int.parse(match.group(1)!), + int.parse(match.group(2)!), + int.parse(match.group(3)!), + pre: match.group(4), + build: match.group(5), + ); + } + + final relaxedMatch = _semverRegexReleaxed.firstMatch(containsVersion); + if (relaxedMatch != null) { + return Version( + int.parse(relaxedMatch.group(1)!), + int.parse(relaxedMatch.group(2) ?? '0'), + int.parse(relaxedMatch.group(3) ?? '0'), + pre: relaxedMatch.group(4), + build: relaxedMatch.group(5), + ); + } + + return null; +} + +final _semverRegex = RegExp( + r'(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?'); + +final _semverRegexReleaxed = RegExp( + r'(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?');
diff --git a/pkgs/native_toolchain_c/pubspec.yaml b/pkgs/native_toolchain_c/pubspec.yaml new file mode 100644 index 0000000..886e745 --- /dev/null +++ b/pkgs/native_toolchain_c/pubspec.yaml
@@ -0,0 +1,28 @@ +name: native_toolchain_c +description: >- + A library to invoke the native C compiler installed on the host machine. +version: 0.3.2 +repository: https://github.com/dart-lang/native/tree/main/pkgs/native_toolchain_c + +topics: + - compiler + - ffi + - interop + - native-assets + - native-toolchain + +environment: + sdk: '>=3.1.0 <4.0.0' + +dependencies: + cli_config: ^0.1.1 + glob: ^2.1.1 + logging: ^1.1.1 + meta: ^1.9.1 + native_assets_cli: ^0.3.0 + pub_semver: ^2.1.3 + +dev_dependencies: + collection: ^1.17.1 + dart_flutter_team_lints: ^2.1.1 + test: ^1.21.0
diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_build_failure_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_build_failure_test.dart new file mode 100644 index 0000000..f2a73c4 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_build_failure_test.dart
@@ -0,0 +1,62 @@ +// 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. + +@OnPlatform({ + 'mac-os': Timeout.factor(2), + 'windows': Timeout.factor(10), +}) +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'; + +void main() { + test('build failure', () async { + final tempUri = await tempDirForTest(); + final addCOriginalUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + final addCUri = tempUri.resolve('add.c'); + final addCOriginalContents = + await File.fromUri(addCOriginalUri).readAsString(); + final addCBrokenContents = addCOriginalContents.replaceAll( + 'int32_t a, int32_t b', 'int64_t blabla'); + await File.fromUri(addCUri).writeAsString(addCBrokenContents); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + linkModePreference: LinkModePreference.dynamic, + buildMode: BuildMode.release, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + sources: [addCUri.toFilePath()], + name: name, + assetId: name, + ); + expect( + () => cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ), + throwsException, + ); + }); +}
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 new file mode 100644 index 0000000..0f5081a --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart
@@ -0,0 +1,150 @@ +// 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. + +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'; + +void main() { + const targets = [ + Target.androidArm, + Target.androidArm64, + Target.androidIA32, + Target.androidX64, + ]; + + const readElfMachine = { + Target.androidArm: 'ARM', + Target.androidArm64: 'AArch64', + Target.androidIA32: 'Intel 80386', + Target.androidX64: 'Advanced Micro Devices X86-64', + }; + + const objdumpFileFormat = { + Target.androidArm: 'elf32-littlearm', + Target.androidArm64: 'elf64-littleaarch64', + Target.androidIA32: 'elf32-i386', + Target.androidX64: 'elf64-x86-64', + }; + + /// From https://docs.flutter.dev/reference/supported-platforms. + const flutterAndroidNdkVersionLowestBestEffort = 19; + + /// From https://docs.flutter.dev/reference/supported-platforms. + const flutterAndroidNdkVersionLowestSupported = 21; + + /// From https://docs.flutter.dev/reference/supported-platforms. + const flutterAndroidNdkVersionHighestSupported = 34; + + for (final linkMode in LinkMode.values) { + for (final target in targets) { + for (final apiLevel in [ + flutterAndroidNdkVersionLowestBestEffort, + flutterAndroidNdkVersionLowestSupported, + flutterAndroidNdkVersionHighestSupported, + ]) { + test('CBuilder $linkMode library $target minSdkVersion $apiLevel', + () async { + final tempUri = await tempDirForTest(); + final libUri = await buildLib( + tempUri, + target, + apiLevel, + 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:')); + expect(machine, contains(readElfMachine[target])); + } else if (Platform.isMacOS) { + final result = await runProcess( + executable: Uri.file('objdump'), + arguments: ['-T', libUri.path], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = result.stdout + .split('\n') + .firstWhere((e) => e.contains('file format')); + expect(machine, contains(objdumpFileFormat[target])); + } + }); + } + } + } + + test('CBuilder API levels binary difference', () async { + const target = Target.androidArm64; + const linkMode = LinkMode.dynamic; + const apiLevel1 = flutterAndroidNdkVersionLowestSupported; + const apiLevel2 = flutterAndroidNdkVersionHighestSupported; + final tempUri = await tempDirForTest(); + final out1Uri = tempUri.resolve('out1/'); + final out2Uri = tempUri.resolve('out2/'); + final out3Uri = tempUri.resolve('out3/'); + await Directory.fromUri(out1Uri).create(); + await Directory.fromUri(out2Uri).create(); + await Directory.fromUri(out3Uri).create(); + final lib1Uri = await buildLib(out1Uri, target, apiLevel1, linkMode); + final lib2Uri = await buildLib(out2Uri, target, apiLevel2, linkMode); + final lib3Uri = await buildLib(out3Uri, target, apiLevel2, linkMode); + final bytes1 = await File.fromUri(lib1Uri).readAsBytes(); + final bytes2 = await File.fromUri(lib2Uri).readAsBytes(); + final bytes3 = await File.fromUri(lib3Uri).readAsBytes(); + // Different API levels should lead to a different binary. + expect(bytes1, isNot(bytes2)); + // Identical API levels should lead to an identical binary. + expect(bytes2, bytes3); + }); +} + +Future<Uri> buildLib( + Uri tempUri, + Target target, + int androidNdkApi, + LinkMode linkMode, +) async { + final addCUri = packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: target.architecture, + targetOs: target.os, + targetAndroidNdkApi: androidNdkApi, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = tempUri.resolve(target.os.libraryFileName(name, linkMode)); + return libUri; +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_ios_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_ios_test.dart new file mode 100644 index 0000000..586f7ff --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_ios_test.dart
@@ -0,0 +1,151 @@ +// 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. + +@TestOn('mac-os') +@OnPlatform({ + 'mac-os': Timeout.factor(2), +}) +library; + +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'; + +void main() { + if (!Platform.isMacOS) { + // Avoid needing status files on Dart SDK CI. + return; + } + + const targets = [ + Target.iOSArm64, + Target.iOSX64, + ]; + + // Dont include 'mach-o' or 'Mach-O', different spelling is used. + const objdumpFileFormat = { + Target.iOSArm64: 'arm64', + Target.iOSX64: '64-bit x86-64', + }; + + const name = 'add'; + + for (final linkMode in LinkMode.values) { + for (final targetIOSSdk in IOSSdk.values) { + for (final target in targets) { + if (target == Target.iOSX64 && targetIOSSdk == IOSSdk.iPhoneOs) { + continue; + } + + final libName = target.os.libraryFileName(name, linkMode); + for (final installName in [ + null, + if (linkMode == LinkMode.dynamic) + Uri.file('@executable_path/Frameworks/$libName'), + ]) { + test( + 'CBuilder $linkMode library $targetIOSSdk $target' + ' ${installName ?? ''}' + .trim(), () async { + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: target.architecture, + targetOs: target.os, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + targetIOSSdk: targetIOSSdk, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + installName: installName, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = tempUri.resolve(libName); + final objdumpResult = await runProcess( + executable: Uri.file('objdump'), + arguments: ['-t', libUri.path], + logger: logger, + ); + expect(objdumpResult.exitCode, 0); + final machine = objdumpResult.stdout + .split('\n') + .firstWhere((e) => e.contains('file format')); + expect(machine, contains(objdumpFileFormat[target])); + + final otoolResult = await runProcess( + executable: Uri.file('otool'), + arguments: ['-l', libUri.path], + logger: logger, + ); + expect(otoolResult.exitCode, 0); + if (targetIOSSdk == IOSSdk.iPhoneOs || target == Target.iOSX64) { + // The x64 simulator behaves as device, presumably because the + // devices are never x64. + expect(otoolResult.stdout, contains('LC_VERSION_MIN_IPHONEOS')); + expect(otoolResult.stdout, isNot(contains('LC_BUILD_VERSION'))); + } else { + expect(otoolResult.stdout, + isNot(contains('LC_VERSION_MIN_IPHONEOS'))); + expect(otoolResult.stdout, contains('LC_BUILD_VERSION')); + final platform = otoolResult.stdout + .split('\n') + .firstWhere((e) => e.contains('platform')); + const platformIosSimulator = 7; + expect(platform, contains(platformIosSimulator.toString())); + } + + if (linkMode == LinkMode.dynamic) { + final libInstallName = await runOtoolInstallName(libUri, libName); + if (installName == null) { + // If no install path is passed, we have an absolute path. + final tempName = tempUri.pathSegments.lastWhere((e) => e != ''); + final pathEnding = + Uri.directory(tempName).resolve(libName).toFilePath(); + expect(Uri.file(libInstallName).isAbsolute, true); + expect(libInstallName, contains(pathEnding)); + final targetInstallName = + '@executable_path/Frameworks/$libName'; + await runProcess( + executable: Uri.file('install_name_tool'), + arguments: [ + '-id', + targetInstallName, + libUri.toFilePath(), + ], + logger: logger, + ); + final libInstallName2 = + await runOtoolInstallName(libUri, libName); + expect(libInstallName2, targetInstallName); + } else { + expect(libInstallName, installName.toFilePath()); + } + } + }); + } + } + } + } +}
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 new file mode 100644 index 0000000..8abe7a4 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart
@@ -0,0 +1,86 @@ +// 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. + +@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:native_toolchain_c/src/utils/run_process.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + if (!Platform.isLinux) { + // Avoid needing status files on Dart SDK CI. + return; + } + + const targets = [ + Target.linuxArm, + Target.linuxArm64, + Target.linuxIA32, + Target.linuxX64, + Target.linuxRiscv64, + ]; + + const readElfMachine = { + Target.linuxArm: 'ARM', + Target.linuxArm64: 'AArch64', + Target.linuxIA32: 'Intel 80386', + Target.linuxX64: 'Advanced Micro Devices X86-64', + Target.linuxRiscv64: 'RISC-V', + }; + + for (final linkMode in LinkMode.values) { + for (final target in targets) { + test('CBuilder $linkMode library $target', () async { + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: target.architecture, + targetOs: target.os, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = + tempUri.resolve(target.os.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:')); + expect(machine, contains(readElfMachine[target])); + expect(result.exitCode, 0); + }); + } + } +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_macos_host_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_macos_host_test.dart new file mode 100644 index 0000000..655ea25 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_macos_host_test.dart
@@ -0,0 +1,84 @@ +// 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. + +@TestOn('mac-os') +@OnPlatform({ + 'mac-os': Timeout.factor(2), +}) +library; + +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'; + +void main() { + if (!Platform.isMacOS) { + // Avoid needing status files on Dart SDK CI. + return; + } + + const targets = [ + Target.macOSArm64, + Target.macOSX64, + ]; + + // Dont include 'mach-o' or 'Mach-O', different spelling is used. + const objdumpFileFormat = { + Target.macOSArm64: 'arm64', + Target.macOSX64: '64-bit x86-64', + }; + + for (final linkMode in LinkMode.values) { + for (final target in targets) { + test('CBuilder $linkMode library $target', () async { + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: target.architecture, + targetOs: target.os, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = + tempUri.resolve(target.os.libraryFileName(name, linkMode)); + final result = await runProcess( + executable: Uri.file('objdump'), + arguments: ['-t', libUri.path], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = result.stdout + .split('\n') + .firstWhere((e) => e.contains('file format')); + expect(machine, contains(objdumpFileFormat[target])); + }); + } + } +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_windows_host_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_windows_host_test.dart new file mode 100644 index 0000000..bf0e836 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_windows_host_test.dart
@@ -0,0 +1,100 @@ +// 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. + +@TestOn('windows') +@OnPlatform({ + 'windows': Timeout.factor(10), +}) +library; + +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/native_toolchain/msvc.dart'; +import 'package:native_toolchain_c/src/utils/run_process.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + if (!Platform.isWindows) { + // Avoid needing status files on Dart SDK CI. + return; + } + + const targets = [ + Target.windowsIA32, + Target.windowsX64, + ]; + + late Uri dumpbinUri; + + setUp(() async { + dumpbinUri = + (await dumpbin.defaultResolver!.resolve(logger: logger)).first.uri; + }); + + const dumpbinMachine = { + Target.windowsIA32: 'x86', + Target.windowsX64: 'x64', + }; + + const dumpbinFileType = { + LinkMode.dynamic: 'DLL', + LinkMode.static: 'LIBRARY', + }; + + for (final linkMode in LinkMode.values) { + for (final target in targets) { + test('CBuilder $linkMode library $target', () async { + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetOs: target.os, + targetArchitecture: target.architecture, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = + tempUri.resolve(target.os.libraryFileName(name, linkMode)); + expect(await File.fromUri(libUri).exists(), true); + final result = await runProcess( + executable: dumpbinUri, + arguments: ['/HEADERS', libUri.toFilePath()], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = + result.stdout.split('\n').firstWhere((e) => e.contains('machine')); + expect(machine, contains(dumpbinMachine[target])); + final fileType = result.stdout + .split('\n') + .firstWhere((e) => e.contains('File Type')); + expect(fileType, contains(dumpbinFileType[linkMode])); + }); + } + } +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_test.dart new file mode 100644 index 0000000..53b5a96 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_test.dart
@@ -0,0 +1,593 @@ +// 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. + +@OnPlatform({ + 'mac-os': Timeout.factor(2), + 'windows': Timeout.factor(10), +}) +library; + +import 'dart:ffi'; +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'; + +void main() { + test('Langauge.toString', () { + expect(Language.c.toString(), 'c'); + expect(Language.cpp.toString(), 'c++'); + }); + + for (final pic in [null, true, false]) { + final picTag = + switch (pic) { null => 'auto_pic', true => 'pic', false => 'no_pic' }; + + for (final buildMode in BuildMode.values) { + final suffix = testSuffix([buildMode, picTag]); + + test('CBuilder executable$suffix', () async { + final tempUri = await tempDirForTest(); + final helloWorldCUri = packageUri + .resolve('test/cbuilder/testfiles/hello_world/src/hello_world.c'); + if (!await File.fromUri(helloWorldCUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'hello_world'; + + final logMessages = <String>[]; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: buildMode, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.executable( + name: name, + sources: [helloWorldCUri.toFilePath()], + pie: pic, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + if (buildMode == BuildMode.debug) { + expect(result.stdout.trim(), startsWith('Running in debug mode.')); + } + expect(result.stdout.trim(), endsWith('Hello world.')); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(helloWorldCUri.toFilePath()), + ); + + switch ((buildConfig.targetOs, pic)) { + case (OS.windows, _) || (_, null): + expect(compilerInvocation, isNot(contains('-fPIC'))); + expect(compilerInvocation, isNot(contains('-fPIE'))); + expect(compilerInvocation, isNot(contains('-fno-PIC'))); + expect(compilerInvocation, isNot(contains('-fno-PIE'))); + case (_, true): + expect(compilerInvocation, contains('-fPIE')); + case (_, false): + expect(compilerInvocation, contains('-fno-PIC')); + expect(compilerInvocation, contains('-fno-PIE')); + } + }); + } + + for (final dryRun in [true, false]) { + final suffix = testSuffix([if (dryRun) 'dry_run', picTag]); + + test('CBuilder dylib$suffix', () async { + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final logMessages = <String>[]; + final logger = createCapturingLogger(logMessages); + + final buildConfig = dryRun + ? BuildConfig.dryRun( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetOs: OS.current, + linkModePreference: LinkModePreference.dynamic, + ) + : BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + sources: [addCUri.toFilePath()], + name: name, + assetId: name, + pic: pic, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final dylibUri = tempUri.resolve(Target.current.os.dylibFileName(name)); + expect(await File.fromUri(dylibUri).exists(), !dryRun); + if (!dryRun) { + final dylib = openDynamicLibraryForTest(dylibUri.toFilePath()); + final add = dylib.lookupFunction<Int32 Function(Int32, Int32), + int Function(int, int)>('add'); + expect(add(1, 2), 3); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(addCUri.toFilePath()), + ); + switch ((buildConfig.targetOs, pic)) { + case (OS.windows, _) || (_, null): + expect(compilerInvocation, isNot(contains('-fPIC'))); + expect(compilerInvocation, isNot(contains('-fPIE'))); + expect(compilerInvocation, isNot(contains('-fno-PIC'))); + expect(compilerInvocation, isNot(contains('-fno-PIE'))); + case (_, true): + expect(compilerInvocation, contains('-fPIC')); + case (_, false): + expect(compilerInvocation, contains('-fno-PIC')); + expect(compilerInvocation, contains('-fno-PIE')); + } + } + }); + } + } + + for (final buildMode in BuildMode.values) { + for (final enabled in [true, false]) { + final suffix = testSuffix([buildMode, enabled ? 'enabled' : 'disabled']); + + test( + 'CBuilder build mode defines$suffix', + () => testDefines( + buildMode: buildMode, + buildModeDefine: enabled, + ndebugDefine: enabled, + ), + ); + } + } + + for (final value in [true, false]) { + final suffix = testSuffix([value ? 'with_value' : 'without_value']); + + test( + 'CBuilder define$suffix', + () => testDefines(customDefineWithValue: value), + ); + } + + test('CBuilder flags', () async { + final tempUri = await tempDirForTest(); + final definesCUri = + packageUri.resolve('test/cbuilder/testfiles/defines/src/defines.c'); + if (!await File.fromUri(definesCUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'defines'; + + final logMessages = <String>[]; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final flag = switch (buildConfig.targetOs) { + OS.windows => '/DFOO=USER_FLAG', + _ => '-DFOO=USER_FLAG', + }; + + final cbuilder = CBuilder.executable( + name: name, + sources: [definesCUri.toFilePath()], + flags: [flag], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + expect(result.stdout, contains('Macro FOO is defined: USER_FLAG')); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(definesCUri.toFilePath()), + ); + expect(compilerInvocation, contains(flag)); + }); + + test('CBuilder includes', () async { + final tempUri = await tempDirForTest(); + final includeDirectoryUri = + packageUri.resolve('test/cbuilder/testfiles/includes/include'); + final includesHUri = packageUri + .resolve('test/cbuilder/testfiles/includes/include/includes.h'); + final includesCUri = + packageUri.resolve('test/cbuilder/testfiles/includes/src/includes.c'); + const name = 'includes'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetId: name, + includes: [includeDirectoryUri.toFilePath()], + sources: [includesCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + expect(buildOutput.dependencies.dependencies, contains(includesHUri)); + + final dylibUri = tempUri.resolve(Target.current.os.dylibFileName(name)); + final dylib = openDynamicLibraryForTest(dylibUri.toFilePath()); + final x = dylib.lookup<Int>('x'); + expect(x.value, 42); + }); + + test('CBuilder std', () async { + final tempUri = await tempDirForTest(); + final addCUri = packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + const std = 'c99'; + + final logMessages = <String>[]; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final stdFlag = switch (buildConfig.targetOs) { + OS.windows => '/std:$std', + _ => '-std=$std', + }; + + final cbuilder = CBuilder.library( + sources: [addCUri.toFilePath()], + name: name, + assetId: name, + std: std, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final dylibUri = tempUri.resolve(Target.current.os.dylibFileName(name)); + + final dylib = openDynamicLibraryForTest(dylibUri.toFilePath()); + final add = dylib.lookupFunction<Int32 Function(Int32, Int32), + int Function(int, int)>('add'); + expect(add(1, 2), 3); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(addCUri.toFilePath()), + ); + expect(compilerInvocation, contains(stdFlag)); + }); + + test('CBuilder compile c++', () async { + final tempUri = await tempDirForTest(); + final helloWorldCppUri = packageUri.resolve( + 'test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc'); + if (!await File.fromUri(helloWorldCppUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'hello_world_cpp'; + + final logMessages = <String>[]; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + buildMode: BuildMode.release, + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final defaultStdLibLinkFlag = switch (buildConfig.targetOs) { + OS.windows => null, + OS.linux => '-l stdc++', + OS.macOS => '-l c++', + _ => throw UnimplementedError(), + }; + + final cbuilder = CBuilder.executable( + name: name, + sources: [helloWorldCppUri.toFilePath()], + language: Language.cpp, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + expect(result.stdout.trim(), endsWith('Hello world.')); + + if (defaultStdLibLinkFlag != null) { + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(helloWorldCppUri.toFilePath()), + ); + expect(compilerInvocation, contains(defaultStdLibLinkFlag)); + } + }); + + test('CBuilder cppLinkStdLib', () async { + final tempUri = await tempDirForTest(); + final helloWorldCppUri = packageUri.resolve( + 'test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc'); + if (!await File.fromUri(helloWorldCppUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'hello_world_cpp'; + + final logMessages = <String>[]; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + buildMode: BuildMode.release, + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.executable( + name: name, + sources: [helloWorldCppUri.toFilePath()], + language: Language.cpp, + cppLinkStdLib: 'stdc++', + ); + + if (buildConfig.targetOs == OS.windows) { + await expectLater( + () => cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ), + throwsArgumentError, + ); + } else { + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + expect(result.stdout.trim(), endsWith('Hello world.')); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(helloWorldCppUri.toFilePath()), + ); + expect(compilerInvocation, contains('-l stdc++')); + } + }); +} + +Future<void> testDefines({ + BuildMode buildMode = BuildMode.debug, + bool buildModeDefine = false, + bool ndebugDefine = false, + bool? customDefineWithValue, +}) async { + final tempUri = await tempDirForTest(); + final definesCUri = + packageUri.resolve('test/cbuilder/testfiles/defines/src/defines.c'); + if (!await File.fromUri(definesCUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'defines'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: name, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: buildMode, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.executable( + name: name, + sources: [definesCUri.toFilePath()], + defines: { + if (customDefineWithValue != null) + 'FOO': customDefineWithValue ? 'BAR' : null, + }, + buildModeDefine: buildModeDefine, + ndebugDefine: ndebugDefine, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + + if (buildModeDefine) { + expect( + result.stdout, + contains('Macro ${buildMode.name.toUpperCase()} is defined: 1'), + ); + } else { + expect( + result.stdout, + contains('Macro ${buildMode.name.toUpperCase()} is undefined.'), + ); + } + + if (ndebugDefine && buildMode != BuildMode.debug) { + expect( + result.stdout, + contains('Macro NDEBUG is defined: 1'), + ); + } else { + expect( + result.stdout, + contains('Macro NDEBUG is undefined.'), + ); + } + + if (customDefineWithValue != null) { + expect( + result.stdout, + contains( + 'Macro FOO is defined: ${customDefineWithValue ? 'BAR' : '1'}', + ), + ); + } else { + expect( + result.stdout, + contains('Macro FOO is undefined.'), + ); + } +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart new file mode 100644 index 0000000..b77c231 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart
@@ -0,0 +1,84 @@ +// 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. + +@OnPlatform({ + 'mac-os': Timeout.factor(2), + 'windows': Timeout.factor(10), +}) +library; + +import 'package:collection/collection.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:native_toolchain_c/src/cbuilder/compiler_resolver.dart'; +import 'package:native_toolchain_c/src/native_toolchain/apple_clang.dart'; +import 'package:native_toolchain_c/src/native_toolchain/clang.dart'; +import 'package:native_toolchain_c/src/native_toolchain/msvc.dart'; +import 'package:native_toolchain_c/src/tool/tool_error.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('Config provided compiler', () async { + final tempUri = await tempDirForTest(); + final ar = [ + ...await appleAr.defaultResolver!.resolve(logger: logger), + ...await lib.defaultResolver!.resolve(logger: logger), + ...await llvmAr.defaultResolver!.resolve(logger: logger), + ].first.uri; + final cc = [ + ...await appleClang.defaultResolver!.resolve(logger: logger), + ...await cl.defaultResolver!.resolve(logger: logger), + ...await clang.defaultResolver!.resolve(logger: logger), + ].first.uri; + final ld = [ + ...await appleLd.defaultResolver!.resolve(logger: logger), + ...await link.defaultResolver!.resolve(logger: logger), + ...await lld.defaultResolver!.resolve(logger: logger), + ].first.uri; + final envScript = [ + ...await vcvars64.defaultResolver!.resolve(logger: logger) + ].firstOrNull?.uri; + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: 'dummy', + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + ar: ar, + cc: cc, + ld: ld, + envScript: envScript, + ), + ); + final resolver = CompilerResolver(buildConfig: buildConfig, logger: logger); + final compiler = await resolver.resolveCompiler(); + final archiver = await resolver.resolveArchiver(); + expect(compiler.uri, buildConfig.cCompiler.cc); + expect(archiver.uri, buildConfig.cCompiler.ar); + }); + + test('No compiler found', () async { + final tempUri = await tempDirForTest(); + final buildConfig = BuildConfig( + outDir: tempUri, + packageName: 'dummy', + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.windows, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + ); + final resolver = CompilerResolver( + buildConfig: buildConfig, + logger: logger, + host: Target.androidArm64, // This is never a host. + ); + expect(resolver.resolveCompiler, throwsA(isA<ToolError>())); + expect(resolver.resolveArchiver, throwsA(isA<ToolError>())); + }); +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/add/src/add.c b/pkgs/native_toolchain_c/test/cbuilder/testfiles/add/src/add.c new file mode 100644 index 0000000..6a16cca --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/add/src/add.c
@@ -0,0 +1,22 @@ +// 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. + +#include <stdint.h> + +#ifdef DEBUG +#include <stdio.h> +#endif + +#if _WIN32 +#define FFI_EXPORT __declspec(dllexport) +#else +#define FFI_EXPORT +#endif + +FFI_EXPORT int32_t add(int32_t a, int32_t b) { +#ifdef DEBUG + printf("Adding %i and %i.\n", a, b); +#endif + return a + b; +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/defines/src/defines.c b/pkgs/native_toolchain_c/test/cbuilder/testfiles/defines/src/defines.c new file mode 100644 index 0000000..7bb554b --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/defines/src/defines.c
@@ -0,0 +1,38 @@ +// 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. + +#include <stdio.h> + +#define STRINGIFY(X) #X +#define MACRO_IS_UNDEFINED(name) printf("Macro " #name " is undefined.\n"); +#define MACRO_IS_DEFINED(name) \ + printf("Macro " #name " is defined: " STRINGIFY(name) "\n"); + +int main() { +#ifdef DEBUG + MACRO_IS_DEFINED(DEBUG); +#else + MACRO_IS_UNDEFINED(DEBUG); +#endif + +#ifdef RELEASE + MACRO_IS_DEFINED(RELEASE); +#else + MACRO_IS_UNDEFINED(RELEASE); +#endif + +#ifdef NDEBUG + MACRO_IS_DEFINED(NDEBUG); +#else + MACRO_IS_UNDEFINED(NDEBUG); +#endif + +#ifdef FOO + MACRO_IS_DEFINED(FOO); +#else + MACRO_IS_UNDEFINED(FOO); +#endif + + return 0; +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world/src/hello_world.c b/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world/src/hello_world.c new file mode 100644 index 0000000..2d42059 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world/src/hello_world.c
@@ -0,0 +1,13 @@ +// 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. + +#include <stdio.h> + +int main() { +#ifdef DEBUG + printf("Running in debug mode.\n"); +#endif + printf("Hello world.\n"); + return 0; +}
diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc b/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc new file mode 100644 index 0000000..d883a32 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc
@@ -0,0 +1,14 @@ +// 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. + +#include <iostream> + +int main() { +#ifdef DEBUG + std::cout << "Running in debug mode." << std::endl; +#endif + std::cout << "Hello world." << std::endl; + return 0; +} +
diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/include/includes.h b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/include/includes.h new file mode 100644 index 0000000..c2f78a8 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/include/includes.h
@@ -0,0 +1,12 @@ +// 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. + +#if _WIN32 +#define FFI_EXPORT __declspec(dllexport) +#else +#define FFI_EXPORT +#endif + +FFI_EXPORT int x = 42; +
diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/src/includes.c b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/src/includes.c new file mode 100644 index 0000000..851907f --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/src/includes.c
@@ -0,0 +1,6 @@ +// 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. + +#include "includes.h" +
diff --git a/pkgs/native_toolchain_c/test/helpers.dart b/pkgs/native_toolchain_c/test/helpers.dart new file mode 100644 index 0000000..abb157b --- /dev/null +++ b/pkgs/native_toolchain_c/test/helpers.dart
@@ -0,0 +1,182 @@ +// 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. + +import 'dart:async'; +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'; + +/// Returns a suffix for a test that is parameterized. +/// +/// [tags] represent the current configuration of the test. Each element +/// is converted to a string by calling [Object.toString]. +/// +/// ## Example +/// +/// The instances of the test below will have the following descriptions: +/// +/// - `My test` +/// - `My test (dry_run)` +/// +/// ```dart +/// void main() { +/// for (final dryRun in [true, false]) { +/// final suffix = testSuffix([if (dryRun) 'dry_run']); +/// +/// test('My test$suffix', () {}); +/// } +/// } +/// ``` +String testSuffix(List<Object> tags) => switch (tags) { + [] => '', + _ => ' (${tags.join(', ')})', + }; + +const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; + +Future<Uri> tempDirForTest({String? prefix, bool keepTemp = false}) async { + final tempDir = await Directory.systemTemp.createTemp(prefix); + // Deal with Windows temp folder aliases. + final tempUri = + Directory(await tempDir.resolveSymbolicLinks()).uri.normalizePath(); + if ((!Platform.environment.containsKey(keepTempKey) || + Platform.environment[keepTempKey]!.isEmpty) && + !keepTemp) { + addTearDown(() => tempDir.delete(recursive: true)); + } + return tempUri; +} + +/// Logger that outputs the full trace when a test fails. +Logger get logger => _logger ??= () { + // A new logger is lazily created for each test so that the messages + // captured by printOnFailure are scoped to the correct test. + addTearDown(() => _logger = null); + return _createTestLogger(); + }(); + +Logger? _logger; + +Logger createCapturingLogger(List<String> capturedMessages) => + _createTestLogger(capturedMessages: capturedMessages); + +Logger _createTestLogger({List<String>? capturedMessages}) => + Logger.detached('') + ..level = Level.ALL + ..onRecord.listen((record) { + printOnFailure( + '${record.level.name}: ${record.time}: ${record.message}'); + capturedMessages?.add(record.message); + }); + +/// Test files are run in a variety of ways, find this package root in all. +/// +/// Test files can be run from source from any working directory. The Dart SDK +/// `tools/test.py` runs them from the root of the SDK for example. +/// +/// Test files can be run from dill from the root of package. `package:test` +/// does this. +/// +/// https://github.com/dart-lang/test/issues/110 +Uri findPackageRoot(String packageName) { + final script = Platform.script; + final fileName = script.name; + if (fileName.endsWith('_test.dart')) { + // We're likely running from source. + var directory = script.resolve('.'); + while (true) { + final dirName = directory.name; + if (dirName == packageName) { + return directory; + } + final parent = directory.resolve('..'); + if (parent == directory) break; + directory = parent; + } + } else if (fileName.endsWith('.dill')) { + final cwd = Directory.current.uri; + final dirName = cwd.name; + if (dirName == packageName) { + return cwd; + } + } + throw StateError("Could not find package root for package '$packageName'. " + 'Tried finding the package root via Platform.script ' + "'${Platform.script.toFilePath()}' and Directory.current " + "'${Directory.current.uri.toFilePath()}'."); +} + +Uri packageUri = findPackageRoot('native_toolchain_c'); + +extension on Uri { + String get name => pathSegments.where((e) => e != '').last; +} + +String unparseKey(String key) => key.replaceAll('.', '__').toUpperCase(); + +/// Archiver provided by the environment. +final Uri? ar = Platform + .environment[unparseKey(CCompilerConfig.arConfigKeyFull)] + ?.asFileUri(); + +/// Compiler provided by the environment. +final Uri? cc = Platform + .environment[unparseKey(CCompilerConfig.ccConfigKeyFull)] + ?.asFileUri(); + +/// Linker provided by the environment. +final Uri? ld = Platform + .environment[unparseKey(CCompilerConfig.ldConfigKeyFull)] + ?.asFileUri(); + +/// Path to script that sets environment variables for [cc], [ld], and [ar]. +/// +/// Provided by environment. +final Uri? envScript = Platform + .environment[unparseKey(CCompilerConfig.envScriptConfigKeyFull)] + ?.asFileUri(); + +/// Arguments for [envScript] provided by environment. +final List<String>? envScriptArgs = Platform + .environment[unparseKey(CCompilerConfig.envScriptArgsConfigKeyFull)] + ?.split(' '); + +extension on String { + Uri asFileUri() => Uri.file(this); +} + +/// Looks up the install name of a dynamic library at [libraryUri]. +/// +/// Because `otool` output multiple names, [libraryName] as search parameter. +Future<String> runOtoolInstallName(Uri libraryUri, String libraryName) async { + final otoolUri = + (await otool.defaultResolver!.resolve(logger: logger)).first.uri; + final otoolResult = await runProcess( + executable: otoolUri, + arguments: ['-l', libraryUri.path], + logger: logger, + ); + expect(otoolResult.exitCode, 0); + // Leading space on purpose to differentiate from other types of names. + const installNameName = ' name '; + final installName = otoolResult.stdout + .split('\n') + .firstWhere((e) => e.contains(installNameName) && e.contains(libraryName)) + .trim() + .split(' ')[1]; + return installName; +} + +/// Opens the [DynamicLibrary] at [path] and register a tear down hook to close +/// it when the current test is done. +DynamicLibrary openDynamicLibraryForTest(String path) { + final library = DynamicLibrary.open(path); + addTearDown(library.close); + return library; +}
diff --git a/pkgs/native_toolchain_c/test/native_toolchain/apple_clang_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/apple_clang_test.dart new file mode 100644 index 0000000..14829cb --- /dev/null +++ b/pkgs/native_toolchain_c/test/native_toolchain/apple_clang_test.dart
@@ -0,0 +1,58 @@ +// 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. + +@TestOn('mac-os') +@OnPlatform({ + 'mac-os': Timeout.factor(2), +}) +library; + +import 'dart:io'; + +import 'package:native_toolchain_c/src/native_toolchain/apple_clang.dart'; +import 'package:native_toolchain_c/src/tool/tool_requirement.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + if (!Platform.isMacOS) { + // Avoid needing status files on Dart SDK CI. + return; + } + + test('smoke test', () async { + final requirement = ToolRequirement(appleClang, + minimumVersion: Version(12, 0, 0, pre: '0')); + final resolved = await appleClang.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); + + test('ar test', () async { + final requirement = ToolRequirement(appleAr); + final resolved = await appleAr.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); + + test('ld test', () async { + final requirement = ToolRequirement(appleLd); + final resolved = await appleLd.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); + + test('otool test', () async { + final requirement = ToolRequirement(otool); + final resolved = await otool.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); +}
diff --git a/pkgs/native_toolchain_c/test/native_toolchain/clang_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/clang_test.dart new file mode 100644 index 0000000..88be30c --- /dev/null +++ b/pkgs/native_toolchain_c/test/native_toolchain/clang_test.dart
@@ -0,0 +1,60 @@ +// 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. + +@TestOn('linux') +library; + +import 'dart:io'; + +import 'package:native_toolchain_c/src/native_toolchain/clang.dart'; +import 'package:native_toolchain_c/src/tool/tool_instance.dart'; +import 'package:native_toolchain_c/src/tool/tool_requirement.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + if (!Platform.isLinux) { + // Avoid needing status files on Dart SDK CI. + return; + } + + test('clang smoke test', () async { + final requirement = + ToolRequirement(clang, minimumVersion: Version(14, 0, 0, pre: '0')); + final resolved = await clang.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); + + test('clang versions', () { + final clangInstance = ToolInstance( + tool: clang, + uri: Uri.file('some/path'), + version: Version.parse('14.0.0-1'), + ); + final requirement = + ToolRequirement(clang, minimumVersion: Version(14, 0, 0, pre: '0')); + final satisfied = requirement.satisfy([clangInstance]); + expect(satisfied?.length, 1); + }); + + test('llvm-ar smoke test', () async { + final requirement = ToolRequirement(llvmAr); + final resolved = await llvmAr.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); + + test('ld test', () async { + final requirement = ToolRequirement(lld); + final resolved = await lld.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); +}
diff --git a/pkgs/native_toolchain_c/test/native_toolchain/gcc_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/gcc_test.dart new file mode 100644 index 0000000..c87f53e --- /dev/null +++ b/pkgs/native_toolchain_c/test/native_toolchain/gcc_test.dart
@@ -0,0 +1,74 @@ +// 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. + +@TestOn('linux') +library; + +import 'dart:io'; + +import 'package:native_toolchain_c/src/native_toolchain/gcc.dart'; +import 'package:native_toolchain_c/src/tool/tool.dart'; +import 'package:native_toolchain_c/src/tool/tool_requirement.dart'; +import 'package:native_toolchain_c/src/tool/tool_resolver.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + if (!Platform.isLinux) { + // Avoid needing status files on Dart SDK CI. + return; + } + + void testToolSet(String name, List<Tool> tools) { + test('gcc cross compilation $name smoke test', () async { + final resolver = ToolResolvers([ + for (final tool in tools) tool.defaultResolver!, + ]); + + final resolved = await resolver.resolve(logger: logger); + printOnFailure(resolved.toString()); + expect(resolved.isNotEmpty, true); + + final requirement = RequireAll([ + for (final tool in tools) ToolRequirement(tool), + ]); + + final satisfied = requirement.satisfy(resolved); + printOnFailure(tools.toString()); + printOnFailure(satisfied.toString()); + expect(satisfied?.length, tools.length); + }); + } + + testToolSet('aarch64LinuxGnuGcc', [ + aarch64LinuxGnuGcc, + aarch64LinuxGnuGccAr, + aarch64LinuxGnuLd, + ]); + + testToolSet('armLinuxGnueabihfGcc', [ + armLinuxGnueabihfGcc, + armLinuxGnueabihfGccAr, + armLinuxGnueabihfLd, + ]); + + testToolSet('i686LinuxGnuGcc', [ + i686LinuxGnuGcc, + i686LinuxGnuGccAr, + i686LinuxGnuLd, + ]); + + testToolSet('x86_64LinuxGnuGcc', [ + x86_64LinuxGnuGcc, + x86_64LinuxGnuGccAr, + x86_64LinuxGnuLd, + ]); + + testToolSet('riscv64LinuxGnuGcc', [ + riscv64LinuxGnuGcc, + riscv64LinuxGnuGccAr, + riscv64LinuxGnuLd, + ]); +}
diff --git a/pkgs/native_toolchain_c/test/native_toolchain/msvc_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/msvc_test.dart new file mode 100644 index 0000000..7e9e348 --- /dev/null +++ b/pkgs/native_toolchain_c/test/native_toolchain/msvc_test.dart
@@ -0,0 +1,188 @@ +// 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. + +@TestOn('windows') +@OnPlatform({ + 'windows': Timeout.factor(10), +}) +library; + +import 'dart:io'; + +import 'package:native_toolchain_c/src/native_toolchain/msvc.dart'; +import 'package:native_toolchain_c/src/utils/env_from_bat.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + if (!Platform.isWindows) { + // Avoid needing status files on Dart SDK CI. + return; + } + + test('vswhere', () async { + final instances = await vswhere.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('visualStudio', () async { + final instances = + await visualStudio.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('msvc', () async { + final instances = await msvc.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('cl', () async { + final instances = await cl.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('clIA32', () async { + final instances = await clIA32.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('clArm64', () async { + final instances = await clArm64.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('lib', () async { + final instances = await lib.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('libIA32', () async { + final instances = await libIA32.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('libArm64', () async { + final instances = await libArm64.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('link', () async { + final instances = await link.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('linkIA32', () async { + final instances = await linkIA32.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('linkArm64', () async { + final instances = await linkArm64.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('dumpbin', () async { + final instances = await dumpbin.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + }); + + test('vcvars32 from cl.exe', () async { + final clInstances = await clIA32.defaultResolver!.resolve(logger: logger); + expect(clInstances.isNotEmpty, true); + + final instances = await vcvars(clInstances.first) + .defaultResolver! + .resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + expect(instance.tool, vcvars32); + final env = await envFromBat(instance.uri); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); + + test('vcvars64 from cl.exe', () async { + final clInstances = await cl.defaultResolver!.resolve(logger: logger); + expect(clInstances.isNotEmpty, true); + + final instances = await vcvars(clInstances.first) + .defaultResolver! + .resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + expect(instance.tool, vcvars64); + final env = await envFromBat(instance.uri); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); + + test('vcvarsarm64 from cl.exe', () async { + final clInstances = await clArm64.defaultResolver!.resolve(logger: logger); + expect(clInstances.isNotEmpty, true); + + final instances = await vcvars(clInstances.first) + .defaultResolver! + .resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + expect(instance.tool, vcvarsarm64); + final env = await envFromBat(instance.uri); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); + + test('vcvars32', () async { + final instances = await vcvars32.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + final env = await envFromBat(instance.uri); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); + + test('vcvars64', () async { + final instances = await vcvars64.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + final env = await envFromBat(instance.uri); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); + + test('vcvarsarm64', () async { + final instances = + await vcvarsarm64.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + final env = await envFromBat(instance.uri); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); + + test('vcvarsall', () async { + final instances = await vcvarsall.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + final env = await envFromBat( + instance.uri, + arguments: [ + 'x64', + 'uwp', + '10.0', + ], + ); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); + + test('vsDevCmd', () async { + final instances = await vsDevCmd.defaultResolver!.resolve(logger: logger); + expect(instances.isNotEmpty, true); + final instance = instances.first; + final env = await envFromBat(instance.uri); + expect(env['INCLUDE'] != null, true); + expect(env['WindowsSdkDir'] != null, true); // stdio.h + }); +}
diff --git a/pkgs/native_toolchain_c/test/native_toolchain/ndk_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/ndk_test.dart new file mode 100644 index 0000000..f6cde28 --- /dev/null +++ b/pkgs/native_toolchain_c/test/native_toolchain/ndk_test.dart
@@ -0,0 +1,23 @@ +// 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. + +import 'package:native_toolchain_c/src/native_toolchain/android_ndk.dart'; +import 'package:native_toolchain_c/src/tool/tool_requirement.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('NDK smoke test', () async { + final requirement = RequireAll([ + ToolRequirement(androidNdk), + ToolRequirement(androidNdkClang), + ToolRequirement(androidNdkLlvmAr), + ToolRequirement(androidNdkLld), + ]); + final resolved = await androidNdk.defaultResolver!.resolve(logger: logger); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 4); + }); +}
diff --git a/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart new file mode 100644 index 0000000..0f18d39 --- /dev/null +++ b/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart
@@ -0,0 +1,113 @@ +// 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. + +import 'package:collection/collection.dart'; +import 'package:native_toolchain_c/src/native_toolchain/android_ndk.dart'; +import 'package:native_toolchain_c/src/native_toolchain/apple_clang.dart'; +import 'package:native_toolchain_c/src/native_toolchain/clang.dart'; +import 'package:native_toolchain_c/src/native_toolchain/gcc.dart'; +import 'package:native_toolchain_c/src/native_toolchain/msvc.dart'; +import 'package:native_toolchain_c/src/native_toolchain/recognizer.dart'; +import 'package:native_toolchain_c/src/tool/tool.dart'; +import 'package:native_toolchain_c/src/tool/tool_instance.dart'; +import 'package:native_toolchain_c/src/tool/tool_resolver.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() async { + final tests = [ + RecognizerTest(appleAr, ArchiverRecognizer.new), + RecognizerTest(appleClang, CompilerRecognizer.new), + RecognizerTest(appleLd, LinkerRecognizer.new), + RecognizerTest(aarch64LinuxGnuGcc, CompilerRecognizer.new), + RecognizerTest(aarch64LinuxGnuGccAr, ArchiverRecognizer.new), + RecognizerTest(aarch64LinuxGnuLd, LinkerRecognizer.new), + RecognizerTest(androidNdkClang, CompilerRecognizer.new), + RecognizerTest(androidNdkLld, LinkerRecognizer.new), + RecognizerTest(androidNdkLlvmAr, ArchiverRecognizer.new), + RecognizerTest(armLinuxGnueabihfGcc, CompilerRecognizer.new), + RecognizerTest(armLinuxGnueabihfGccAr, ArchiverRecognizer.new), + RecognizerTest(armLinuxGnueabihfLd, LinkerRecognizer.new), + RecognizerTest(cl, CompilerRecognizer.new), + RecognizerTest(clang, CompilerRecognizer.new), + RecognizerTest(i686LinuxGnuGcc, CompilerRecognizer.new), + RecognizerTest(i686LinuxGnuGccAr, ArchiverRecognizer.new), + RecognizerTest(i686LinuxGnuLd, LinkerRecognizer.new), + RecognizerTest(lib, ArchiverRecognizer.new), + RecognizerTest(link, LinkerRecognizer.new), + RecognizerTest(lld, LinkerRecognizer.new), + RecognizerTest(llvmAr, ArchiverRecognizer.new), + RecognizerTest(riscv64LinuxGnuGcc, CompilerRecognizer.new), + RecognizerTest(riscv64LinuxGnuGccAr, ArchiverRecognizer.new), + RecognizerTest(riscv64LinuxGnuLd, LinkerRecognizer.new), + RecognizerTest(x86_64LinuxGnuGcc, CompilerRecognizer.new), + RecognizerTest(x86_64LinuxGnuGccAr, ArchiverRecognizer.new), + RecognizerTest(x86_64LinuxGnuLd, LinkerRecognizer.new), + ]; + + for (final test in tests) { + await test.setUp(); + } + + for (final test in tests) { + test.addTest(); + } + + test('compiler does not exist', () async { + final tempUri = await tempDirForTest(); + final recognizer = CompilerRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, <ToolInstance>[]); + }); + + test('linker does not exist', () async { + final tempUri = await tempDirForTest(); + final recognizer = LinkerRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, <ToolInstance>[]); + }); + + test('archiver does not exist', () async { + final tempUri = await tempDirForTest(); + final recognizer = ArchiverRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, <ToolInstance>[]); + }); +} + +class RecognizerTest { + final Tool tool; + final ToolResolver Function(Uri) recognizer; + late final ToolInstance? toolInstance; + + RecognizerTest(this.tool, this.recognizer); + + Future<void> setUp() async { + toolInstance = (await tool.defaultResolver!.resolve( + logger: null /* no printOnFailure support in setup. */, + )) + .where((element) => element.tool == tool) + .firstOrNull; + } + + void addTest() { + if (toolInstance == null) { + // We only want to test if we would recognize the tool again if it exists + // on the host. Skipping pollutes the stdout, so just don't run the test + // at all. + return; + } + + test( + 'recognize ${tool.name}', + () async { + final recognizer_ = recognizer(toolInstance!.uri); + final toolInstanceAgain = + (await recognizer_.resolve(logger: logger)).first; + expect(toolInstanceAgain, toolInstance); + }, + ); + } +}
diff --git a/pkgs/native_toolchain_c/test/native_toolchain/xcode_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/xcode_test.dart new file mode 100644 index 0000000..208851b --- /dev/null +++ b/pkgs/native_toolchain_c/test/native_toolchain/xcode_test.dart
@@ -0,0 +1,64 @@ +// 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. + +@TestOn('mac-os') +@OnPlatform({ + 'mac-os': Timeout.factor(2), +}) +library; + +import 'dart:io'; + +import 'package:native_toolchain_c/src/native_toolchain/xcode.dart'; +import 'package:native_toolchain_c/src/tool/tool.dart'; +import 'package:native_toolchain_c/src/tool/tool_instance.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + if (!Platform.isMacOS) { + // Avoid needing status files on Dart SDK CI. + return; + } + + test('xcrun', () async { + final resolved = (await xcrun.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == xcrun); + expect(resolved.isNotEmpty, true); + }); + + test('macosxSdk', () async { + final resolved = (await macosxSdk.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == macosxSdk); + expect(resolved.isNotEmpty, true); + }); + + test('iPhoneOSSdk', () async { + final resolved = + (await iPhoneOSSdk.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == iPhoneOSSdk); + expect(resolved.isNotEmpty, true); + }); + + test('iPhoneSimulatorSdk', () async { + final resolved = + (await iPhoneSimulatorSdk.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == iPhoneSimulatorSdk); + expect(resolved.isNotEmpty, true); + }); + + test('non-existing SDK', () async { + final xcrunInstance = + (await xcrun.defaultResolver!.resolve(logger: logger)).first; + final tool = Tool(name: 'non-tool'); + final result = await XCodeSdkResolver.tryResolveSdk( + xcrunInstance: xcrunInstance, + sdk: 'doesnotexist', + tool: tool, + logger: logger, + ); + expect(result, <ToolInstance>[]); + }); +}
diff --git a/pkgs/native_toolchain_c/test/tool/tool_instance_test.dart b/pkgs/native_toolchain_c/test/tool/tool_instance_test.dart new file mode 100644 index 0000000..b4a873b --- /dev/null +++ b/pkgs/native_toolchain_c/test/tool/tool_instance_test.dart
@@ -0,0 +1,90 @@ +// 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. + +import 'package:collection/collection.dart'; +import 'package:native_toolchain_c/src/tool/tool.dart'; +import 'package:native_toolchain_c/src/tool/tool_instance.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('equals and hashCode', () { + final barToolInstance = + ToolInstance(tool: Tool(name: 'bar'), uri: Uri.file('path/to/bar')); + final fooToolInstance = + ToolInstance(tool: Tool(name: 'foo'), uri: Uri.file('path/to/foo')); + + expect(barToolInstance, barToolInstance); + expect(barToolInstance != fooToolInstance, true); + + expect(barToolInstance.hashCode, barToolInstance.hashCode); + expect(barToolInstance.hashCode != fooToolInstance.hashCode, true); + + expect( + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 0), + uri: Uri.file('path/to/bar')) != + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 1), + uri: Uri.file('path/to/bar')), + true); + }); + + test('compareTo', () { + final toolInstances = [ + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(2, 0, 0), + uri: Uri.file('path/to/bar'), + ), + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 0), + uri: Uri.file('path/to/bar')), + ToolInstance( + tool: Tool(name: 'bar'), + uri: Uri.file('path/to/bar'), + ), + ToolInstance( + tool: Tool(name: 'bar'), + uri: Uri.file('path/to/some/other/bar'), + ), + ToolInstance( + tool: Tool(name: 'baz'), + uri: Uri.file('path/to/baz'), + ), + ]; + + final toolInstancesSorted = [...toolInstances]..sort(); + expect( + const DeepCollectionEquality().equals(toolInstancesSorted, toolInstances), + true, + ); + }); + + test('toString', () { + final instance = ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 0), + uri: Uri.file('path/to/bar'), + ); + + expect(instance.toString(), contains('bar')); + expect(instance.toString(), contains('1.0.0')); + expect(instance.toString(), contains('path/to/bar')); + }); + + test('copyWith', () { + final instance = ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 0), + uri: Uri.file('path/to/bar'), + ); + + expect(instance.copyWith(), instance); + expect(instance.copyWith(uri: Uri.file('foo/bar')) != instance, true); + }); +}
diff --git a/pkgs/native_toolchain_c/test/tool/tool_requirement_test.dart b/pkgs/native_toolchain_c/test/tool/tool_requirement_test.dart new file mode 100644 index 0000000..9f802db --- /dev/null +++ b/pkgs/native_toolchain_c/test/tool/tool_requirement_test.dart
@@ -0,0 +1,48 @@ +// 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. + +import 'package:native_toolchain_c/src/tool/tool.dart'; +import 'package:native_toolchain_c/src/tool/tool_instance.dart'; +import 'package:native_toolchain_c/src/tool/tool_requirement.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('toString', () { + final requirement = + ToolRequirement(Tool(name: 'clang'), minimumVersion: Version(10, 0, 0)); + expect(requirement.toString(), contains('clang')); + expect(requirement.toString(), contains('10.0.0')); + }); + + test('RequireOne', () { + final requirement = RequireOne([ + ToolRequirement(Tool(name: 'bar'), minimumVersion: Version(10, 0, 0)), + ToolRequirement(Tool(name: 'foo'), minimumVersion: Version(10, 0, 0)), + ]); + final toolInstances = [ + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(10, 0, 0), + uri: Uri.file('path/to/bar'), + ), + ToolInstance( + tool: Tool(name: 'foo'), + version: Version(9, 0, 0), + uri: Uri.file('path/to/foo'), + ), + ]; + final result = requirement.satisfy(toolInstances); + expect( + result, + [ + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(10, 0, 0), + uri: Uri.file('path/to/bar'), + ) + ], + ); + }); +}
diff --git a/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart b/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart new file mode 100644 index 0000000..9f30ce0 --- /dev/null +++ b/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart
@@ -0,0 +1,109 @@ +// 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. + +import 'dart:io'; + +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/native_toolchain/clang.dart'; +import 'package:native_toolchain_c/src/native_toolchain/msvc.dart'; +import 'package:native_toolchain_c/src/tool/tool.dart'; +import 'package:native_toolchain_c/src/tool/tool_error.dart'; +import 'package:native_toolchain_c/src/tool/tool_instance.dart'; +import 'package:native_toolchain_c/src/tool/tool_resolver.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('CliVersionResolver.executableVersion', () async { + final toolInstances = [ + ...await appleClang.defaultResolver!.resolve(logger: logger), + ...await clang.defaultResolver!.resolve(logger: logger), + ...await cl.defaultResolver!.resolve(logger: logger), + ]; + expect(toolInstances.isNotEmpty, true); + final toolInstance = toolInstances.first; + final versionArguments = [ + if (toolInstance.tool != cl) '--version', + ]; + final version = await CliVersionResolver.executableVersion( + toolInstance.uri, + arguments: versionArguments, + logger: logger, + ); + expect(version.major > 5, true); + expect( + () => CliVersionResolver.executableVersion( + toolInstances.first.uri, + arguments: versionArguments, + expectedExitCode: 9999, + logger: logger, + ), + throwsA(isA<ToolError>()), + ); + + try { + await CliVersionResolver.executableVersion( + toolInstances.first.uri, + arguments: versionArguments, + expectedExitCode: 9999, + logger: logger, + ); + // ignore: avoid_catching_errors + } on ToolError catch (e) { + expect(e.toString(), contains('returned unexpected exit code')); + } + }); + + test('RelativeToolResolver', () async { + final tempUri = await tempDirForTest(); + final barExeUri = + tempUri.resolve(Target.current.os.executableFileName('bar')); + final bazExeName = Target.current.os.executableFileName('baz'); + final bazExeUri = tempUri.resolve(bazExeName); + await File.fromUri(barExeUri).writeAsString('dummy'); + await File.fromUri(bazExeUri).writeAsString('dummy'); + expect(await File.fromUri(barExeUri).exists(), true); + expect(await File.fromUri(bazExeUri).exists(), true); + final barResolver = InstallLocationResolver( + toolName: 'bar', + paths: [barExeUri.toFilePath().replaceAll('\\', '/')], + ); + final bazResolver = RelativeToolResolver( + toolName: 'baz', + wrappedResolver: barResolver, + relativePath: Uri.file(bazExeName), + ); + final resolvedBarInstances = await barResolver.resolve(logger: logger); + expect( + resolvedBarInstances, + [ToolInstance(tool: Tool(name: 'bar'), uri: barExeUri)], + ); + final resolvedBazInstances = await bazResolver.resolve(logger: logger); + expect( + resolvedBazInstances, + [ToolInstance(tool: Tool(name: 'baz'), uri: bazExeUri)], + ); + }); + + test('logger', () async { + final tempUri = await tempDirForTest(); + final barExeUri = + tempUri.resolve(Target.current.os.executableFileName('bar')); + final bazExeName = Target.current.os.executableFileName('baz'); + final bazExeUri = tempUri.resolve(bazExeName); + await File.fromUri(barExeUri).writeAsString('dummy'); + final barResolver = InstallLocationResolver( + toolName: 'bar', paths: [barExeUri.toFilePath().replaceAll('\\', '/')]); + final bazResolver = InstallLocationResolver( + toolName: 'baz', paths: [bazExeUri.toFilePath().replaceAll('\\', '/')]); + final barLogs = <String>[]; + final bazLogs = <String>[]; + await barResolver.resolve(logger: createCapturingLogger(barLogs)); + await bazResolver.resolve(logger: createCapturingLogger(bazLogs)); + expect(barLogs.join('\n'), contains('Found [ToolInstance(bar')); + expect(bazLogs.join('\n'), contains('Found no baz')); + }); +}
diff --git a/pkgs/native_toolchain_c/test/tool/tool_test.dart b/pkgs/native_toolchain_c/test/tool/tool_test.dart new file mode 100644 index 0000000..c14aff9 --- /dev/null +++ b/pkgs/native_toolchain_c/test/tool/tool_test.dart
@@ -0,0 +1,27 @@ +// 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. + +import 'package:native_toolchain_c/src/native_toolchain/android_ndk.dart'; +import 'package:native_toolchain_c/src/native_toolchain/clang.dart'; +import 'package:native_toolchain_c/src/tool/tool.dart'; +import 'package:native_toolchain_c/src/tool/tool_resolver.dart'; +import 'package:test/test.dart'; + +void main() { + test('equals and hashCode', () async { + expect(clang, clang); + expect(clang != androidNdk, true); + expect( + Tool(name: 'foo'), + Tool(name: 'foo', defaultResolver: PathToolResolver(toolName: 'foo')), + ); + expect(Tool(name: 'foo') != Tool(name: 'bar'), true); + expect( + Tool(name: 'foo').hashCode, + Tool(name: 'foo', defaultResolver: PathToolResolver(toolName: 'foo')) + .hashCode, + ); + expect(Tool(name: 'foo').hashCode != Tool(name: 'bar').hashCode, true); + }); +}
diff --git a/pkgs/native_toolchain_c/test/utils/run_process_test.dart b/pkgs/native_toolchain_c/test/utils/run_process_test.dart new file mode 100644 index 0000000..21d7c9d --- /dev/null +++ b/pkgs/native_toolchain_c/test/utils/run_process_test.dart
@@ -0,0 +1,47 @@ +// 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. + +import 'dart:io'; + +import 'package:native_toolchain_c/src/utils/run_process.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + final whichUri = Uri.file(Platform.isWindows ? 'where' : 'which'); + + test('log contains working dir', () async { + final tempUri = await tempDirForTest(); + final messages = <String>[]; + await runProcess( + executable: whichUri, + workingDirectory: tempUri, + logger: createCapturingLogger(messages), + ); + expect(messages.join('\n'), contains('cd')); + }); + + test('log contains env', () async { + final messages = <String>[]; + await runProcess( + executable: whichUri, + environment: {'FOO': 'BAR'}, + logger: createCapturingLogger(messages), + ); + expect(messages.join('\n'), contains('FOO=BAR')); + }); + + test('stderr', () async { + final messages = <String>[]; + const filePath = 'a/dart/file/which/does/not/exist.dart'; + final result = await runProcess( + executable: Uri.file(Platform.resolvedExecutable), + arguments: [filePath], + logger: createCapturingLogger(messages), + ); + expect(result.stderr, contains(filePath)); + expect(result.toString(), contains(filePath)); + }); +}