diff --git a/.github/workflows/jnigen.yaml b/.github/workflows/jnigen.yaml index 4be8317664..f21f921ee2 100644 --- a/.github/workflows/jnigen.yaml +++ b/.github/workflows/jnigen.yaml @@ -102,7 +102,7 @@ jobs: run: flutter build apk --target-platform=android-arm64 working-directory: ./pkgs/jnigen/example/notification_plugin/example - name: Run summarizer tests - run: mvn surefire:test + run: ./gradlew test working-directory: ./pkgs/jnigen/java - name: Build summarizer run: dart run jnigen:setup diff --git a/.github/workflows/native.yaml b/.github/workflows/native.yaml index 75d53941f6..e304d14e2d 100644 --- a/.github/workflows/native.yaml +++ b/.github/workflows/native.yaml @@ -14,8 +14,10 @@ on: - "pkgs/data_assets/**" - "pkgs/hooks_runner/**" - "pkgs/hooks/**" + - "pkgs/json_syntax_generator/**" - "pkgs/native_toolchain_c/**" - - "tools/**" + - "pkgs/repo_lint_rules/**" + - "tool/**" push: branches: [main] paths: @@ -24,8 +26,10 @@ on: - "pkgs/data_assets/**" - "pkgs/hooks_runner/**" - "pkgs/hooks/**" + - "pkgs/json_syntax_generator/**" - "pkgs/native_toolchain_c/**" - - "tools/**" + - "pkgs/repo_lint_rules/**" + - "tool/**" schedule: - cron: "0 0 * * 0" # weekly @@ -37,13 +41,12 @@ jobs: os: [ubuntu, macos, windows] sdk: [dev] package: - - code_assets + # TODO(https://github.com/dart-lang/tools/issues/2083): Remove running coverage per package. + - code_assets # currently dubs as place where all packages are run - data_assets - hooks - hooks_runner - native_toolchain_c - # Breaking changes temporarily break the example run on the Dart SDK until hooks_runner is rolled into the Dart SDK dev build. - breaking-change: [false] runs-on: ${{ matrix.os }}-latest @@ -63,65 +66,18 @@ jobs: ndk-version: r27 if: ${{ matrix.os != 'macos' }} - - run: dart pub get - - - run: dart pub get -C test_data/native_add_version_skew/ - if: ${{ matrix.package == 'hooks_runner' }} - - - run: dart pub get -C test_data/native_add_version_skew_2/ - if: ${{ matrix.package == 'hooks_runner' }} - - - 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 . - - 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.os == 'ubuntu' }} - - run: dart test - - - run: dart --enable-experiment=native-assets test - working-directory: pkgs/${{ matrix.package }}/example/build/native_dynamic_linking/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} - - - run: dart --enable-experiment=native-assets test - working-directory: pkgs/${{ matrix.package }}/example/build/native_add_app/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} - - - run: dart --enable-experiment=native-assets run - working-directory: pkgs/${{ matrix.package }}/example/build/native_add_app/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} - - - run: dart --enable-experiment=native-assets build bin/native_add_app.dart - working-directory: pkgs/${{ matrix.package }}/example/build/native_add_app/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} - - - run: ./native_add_app.exe - working-directory: pkgs/${{ matrix.package }}/example/build/native_add_app/bin/native_add_app/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} - - - run: dart --enable-experiment=native-assets test - working-directory: pkgs/${{ matrix.package }}/example/build/use_dart_api/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} - - - run: dart --enable-experiment=native-assets test - working-directory: pkgs/${{ matrix.package }}/example/build/download_asset/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} - - - run: dart --enable-experiment=native-assets test - working-directory: pkgs/${{ matrix.package }}/example/build/system_library/ - if: ${{ matrix.package == 'hooks' && matrix.sdk == 'dev' && !matrix.breaking-change }} + - run: dart pub get - - run: | - dart tool/generate_schemas.dart - dart tool/generate_syntax.dart - dart tool/normalize.dart - git diff --exit-code - working-directory: pkgs/${{ matrix.package }} - if: ${{ matrix.package == 'hook' && matrix.sdk == 'dev' }} + - name: Run pub get, analysis, formatting, generators, tests, and examples. + run: dart tool/ci.dart --pub + working-directory: . + if: ${{ matrix.package == 'code_assets' }} + # TODO(https://github.com/dart-lang/tools/issues/2083): Remove running coverage per package. - name: Install coverage run: dart pub global activate coverage diff --git a/.github/workflows/package_download_asset.yaml b/.github/workflows/package_download_asset.yaml index 6587812a75..e0bc8ce9f2 100644 --- a/.github/workflows/package_download_asset.yaml +++ b/.github/workflows/package_download_asset.yaml @@ -1,5 +1,5 @@ # A workflow that goes together with the example package:download_asset inside -# package:native_assets_cli. +# package:hooks. name: package_download_asset permissions: @@ -10,7 +10,7 @@ on: branches: [ main ] paths: - .github/workflows/package_download_asset.yaml - - pkgs/native_assets_cli/example/build/download_asset/ + - pkgs/hooks/example/build/download_asset/ push: tags: - 'download_asset-prebuild-assets-*' @@ -27,14 +27,14 @@ jobs: defaults: run: - working-directory: pkgs/native_assets_cli/example/build/download_asset/ + working-directory: pkgs/hooks/example/build/download_asset/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c with: - sdk: stable + sdk: dev - uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410 with: @@ -47,7 +47,7 @@ jobs: - run: dart pub get - # Keep this list consistent with pkgs/native_assets_cli/example/build/download_asset/lib/src/hook_helpers/target_versions.dart + # Keep this list consistent with pkgs/hooks/example/build/download_asset/lib/src/hook_helpers/target_versions.dart - name: Build Linux host if: matrix.os == 'ubuntu' run: | @@ -84,9 +84,9 @@ jobs: with: name: ${{ matrix.os }}-host path: | - pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**/*.dll - pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**/*.dylib - pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**/*.so + pkgs/hooks/example/build/download_asset/.dart_tool/download_asset/**/*.dll + pkgs/hooks/example/build/download_asset/.dart_tool/download_asset/**/*.dylib + pkgs/hooks/example/build/download_asset/.dart_tool/download_asset/**/*.so if-no-files-found: error release: @@ -95,7 +95,7 @@ jobs: defaults: run: - working-directory: pkgs/native_assets_cli/example/build/download_asset/ + working-directory: pkgs/hooks/example/build/download_asset/ steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 @@ -106,7 +106,7 @@ jobs: uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e with: merge-multiple: true - path: pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/ + path: pkgs/hooks/example/build/download_asset/.dart_tool/download_asset/ - name: Display structure of downloaded assets run: ls -R .dart_tool/download_asset/ @@ -115,5 +115,5 @@ jobs: uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda if: startsWith(github.ref, 'refs/tags/download_asset-prebuild-assets') with: - files: 'pkgs/native_assets_cli/example/build/download_asset/.dart_tool/download_asset/**' + files: 'pkgs/hooks/example/build/download_asset/.dart_tool/download_asset/**' fail_on_unmatched_files: true diff --git a/.idx/dev.nix b/.idx/dev.nix new file mode 100644 index 0000000000..f775d73d7a --- /dev/null +++ b/.idx/dev.nix @@ -0,0 +1,30 @@ +# To learn more about how to use Nix to configure your environment +# see: https://firebase.google.com/docs/studio/customize-workspace +{ pkgs, ... }: { + channel = "stable-24.11"; + # Use https://search.nixos.org/packages to find packages. + packages = [ + # pkgs.android-tools # Not needed. The Flutter environment already has ~/.androidsdkroot/ + pkgs.clang + pkgs.gcc + pkgs.jdk17 + pkgs.llvm + pkgs.llvmPackages.bintools + # pkgs.pkgsCross.aarch64-android-prebuilt.stdenv.cc # Takes forever to build + # pkgs.pkgsCross.aarch64-multiplatform.gcc # Takes forever to build + ]; + env = { }; + idx = { + # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id". + extensions = [ + "Dart-Code.dart-code" + "mhutchie.git-graph" + "rangav.vscode-thunder-client" + ]; + workspace = { + onCreate = { + dart-install = "dart pub get"; + }; + }; + }; +} diff --git a/pkgs/code_assets/pubspec.yaml b/pkgs/code_assets/pubspec.yaml index c2f8b09c40..d75bf3c3d3 100644 --- a/pkgs/code_assets/pubspec.yaml +++ b/pkgs/code_assets/pubspec.yaml @@ -7,7 +7,7 @@ version: 0.19.0 repository: https://github.com/dart-lang/native/tree/main/pkgs/code_assets -topics: +topics: - assets - ffi - hooks diff --git a/pkgs/hooks/CHANGELOG.md b/pkgs/hooks/CHANGELOG.md index 01cd0bb7c8..ba436e87d7 100644 --- a/pkgs/hooks/CHANGELOG.md +++ b/pkgs/hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.19.1-wip + +- Added links to the `build` and `link` methods as primary entry points. + ## 0.19.0 - Split up `package:native_assets_cli` in `package:hooks`, diff --git a/pkgs/hooks/LICENSE b/pkgs/hooks/LICENSE new file mode 100644 index 0000000000..2b76736b4c --- /dev/null +++ b/pkgs/hooks/LICENSE @@ -0,0 +1,27 @@ +Copyright 2025, 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/hooks/example/build/download_asset/pubspec.yaml b/pkgs/hooks/example/build/download_asset/pubspec.yaml index 19ac282173..5ea3cf9b42 100644 --- a/pkgs/hooks/example/build/download_asset/pubspec.yaml +++ b/pkgs/hooks/example/build/download_asset/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: crypto: ^3.0.6 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: args: ^2.6.0 diff --git a/pkgs/hooks/example/build/native_add_library/pubspec.yaml b/pkgs/hooks/example/build/native_add_library/pubspec.yaml index 29dfadfbb3..7285f8ea2e 100644 --- a/pkgs/hooks/example/build/native_add_library/pubspec.yaml +++ b/pkgs/hooks/example/build/native_add_library/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks/example/build/native_dynamic_linking/pubspec.yaml b/pkgs/hooks/example/build/native_dynamic_linking/pubspec.yaml index 6007ac686d..3983e62671 100644 --- a/pkgs/hooks/example/build/native_dynamic_linking/pubspec.yaml +++ b/pkgs/hooks/example/build/native_dynamic_linking/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks/example/build/system_library/pubspec.yaml b/pkgs/hooks/example/build/system_library/pubspec.yaml index 81ec45b5f2..664a599ecf 100644 --- a/pkgs/hooks/example/build/system_library/pubspec.yaml +++ b/pkgs/hooks/example/build/system_library/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks/example/build/use_dart_api/pubspec.yaml b/pkgs/hooks/example/build/use_dart_api/pubspec.yaml index a7566831b7..a912ac7755 100644 --- a/pkgs/hooks/example/build/use_dart_api/pubspec.yaml +++ b/pkgs/hooks/example/build/use_dart_api/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks/example/link/package_with_assets/assets/unused_asset.json b/pkgs/hooks/example/link/package_with_assets/assets/unused_asset.json index 1a7d134ddc..eb3d36c81c 100644 --- a/pkgs/hooks/example/link/package_with_assets/assets/unused_asset.json +++ b/pkgs/hooks/example/link/package_with_assets/assets/unused_asset.json @@ -1,3 +1,3 @@ { - "freddy": "mercury" + "freddy": "mercury" } diff --git a/pkgs/hooks/example/link/package_with_assets/assets/used_asset.json b/pkgs/hooks/example/link/package_with_assets/assets/used_asset.json index 697721f13a..22ab51818e 100644 --- a/pkgs/hooks/example/link/package_with_assets/assets/used_asset.json +++ b/pkgs/hooks/example/link/package_with_assets/assets/used_asset.json @@ -1,3 +1,3 @@ { - "leroy": "jenkins" + "leroy": "jenkins" } diff --git a/pkgs/hooks/lib/hooks.dart b/pkgs/hooks/lib/hooks.dart index e2430186f6..dbdd9cf73b 100644 --- a/pkgs/hooks/lib/hooks.dart +++ b/pkgs/hooks/lib/hooks.dart @@ -2,10 +2,15 @@ // 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 -/// build hook (`hook/build.dart`). +/// A library that contains the protocol for implementing hooks. +/// +/// The main entrypoint for build hooks (`hook/build.dart`) is [build]. The main +/// entrypoint for link hooks (`hook/link.dart`) is [link]. library; +import 'src/api/build.dart'; +import 'src/api/link.dart'; + export 'src/api/build.dart' show build; export 'src/api/builder.dart' show Builder; export 'src/api/link.dart' show link; diff --git a/pkgs/hooks/pubspec.yaml b/pkgs/hooks/pubspec.yaml index b33f838637..dd2a229914 100644 --- a/pkgs/hooks/pubspec.yaml +++ b/pkgs/hooks/pubspec.yaml @@ -3,7 +3,7 @@ description: >- A library that contains a Dart API for the JSON-based protocol for `hook/build.dart` and `hook/link.dart`. -version: 0.19.0 +version: 0.19.1-wip repository: https://github.com/dart-lang/native/tree/main/pkgs/hooks @@ -28,6 +28,7 @@ dependencies: yaml: ^3.1.3 # Used for reading pubspec.yaml to obtain the package name. dev_dependencies: + args: ^2.6.0 code_assets: ^0.19.0 # Used for running tests with real asset types. custom_lint: ^0.7.5 dart_flutter_team_lints: ^3.5.1 diff --git a/pkgs/hooks/test/helpers.dart b/pkgs/hooks/test/helpers.dart index 6c5d1a0dcb..2b9b2a59a0 100644 --- a/pkgs/hooks/test/helpers.dart +++ b/pkgs/hooks/test/helpers.dart @@ -53,8 +53,8 @@ Future inTempDir( Uri findPackageRoot(String packageName) { final script = Platform.script; final fileName = script.name; - if (fileName.endsWith('_test.dart')) { - // We're likely running from source. + if (fileName.endsWith('.dart')) { + // We're likely running from source in the package somewhere. var directory = script.resolve('.'); while (true) { final dirName = directory.name; @@ -66,12 +66,19 @@ Uri findPackageRoot(String packageName) { directory = parent; } } else if (fileName.endsWith('.dill')) { + // Probably from the package root. final cwd = Directory.current.uri; final dirName = cwd.name; if (dirName == packageName) { return cwd; } } + // Or the workspace root. + final cwd = Directory.current.uri; + final candidate = cwd.resolve('pkgs/$packageName/'); + if (Directory.fromUri(candidate).existsSync()) { + return candidate; + } throw StateError( "Could not find package root for package '$packageName'. " 'Tried finding the package root via Platform.script ' diff --git a/pkgs/hooks/test/schema/helpers.dart b/pkgs/hooks/test/schema/helpers.dart index 7e34424207..91e0080285 100644 --- a/pkgs/hooks/test/schema/helpers.dart +++ b/pkgs/hooks/test/schema/helpers.dart @@ -10,48 +10,7 @@ import 'dart:io'; import 'package:json_schema/json_schema.dart'; import 'package:test/test.dart'; -/// 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('.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()}'.", - ); -} - -extension on Uri { - String get name => pathSegments.where((e) => e != '').last; -} +export '../helpers.dart' show findPackageRoot; typedef AllSchemas = Map; diff --git a/pkgs/hooks/tool/generate_schemas.dart b/pkgs/hooks/tool/generate_schemas.dart index c018bae3a1..5e67c4f09f 100644 --- a/pkgs/hooks/tool/generate_schemas.dart +++ b/pkgs/hooks/tool/generate_schemas.dart @@ -5,12 +5,41 @@ import 'dart:convert'; import 'dart:io'; +import 'package:args/args.dart'; + import '../test/schema/helpers.dart'; import 'normalize.dart'; -void main() { - generateSharedDefinitions(); - generateEntryPoints(); +void main(List args) { + final stopwatch = Stopwatch()..start(); + final parser = ArgParser() + ..addFlag( + 'set-exit-if-changed', + negatable: false, + help: 'Return a non-zero exit code if any files were changed.', + ); + final argResults = parser.parse(args); + final setExitIfChanged = argResults['set-exit-if-changed'] as bool; + + final counts = Counts(); + + generateSharedDefinitions(counts); + generateEntryPoints(counts); + + stopwatch.stop(); + final duration = stopwatch.elapsedMilliseconds / 1000.0; + print( + 'Generated ${counts.generated} files (${counts.changed} changed) in ' + '${duration.toStringAsFixed(2)} seconds.', + ); + if (setExitIfChanged && counts.changed > 0) { + exit(1); + } +} + +class Counts { + int generated = 0; + int changed = 0; } Uri packageUri = findPackageRoot('hooks'); @@ -23,7 +52,7 @@ enum Hook { build, hook, link } const packages = ['hooks', 'code_assets', 'data_assets']; -void generateSharedDefinitions() { +void generateSharedDefinitions(Counts counts) { const hookOutputAssetOverride = { 'properties': { 'assets': { @@ -159,16 +188,28 @@ void generateSharedDefinitions() { }, }, }; - final jsonString = jsonEncoder.convert( - sortJson(contents, schemaUri.path), - ); - File.fromUri(schemaUri).writeAsStringSync('$jsonString\n'); - print('Generated: $schemaUri'); + var jsonString = jsonEncoder.convert(sortJson(contents, schemaUri.path)); + jsonString += '\n'; + final file = File.fromUri(schemaUri); + + var oldContent = ''; + if (file.existsSync()) { + oldContent = file.readAsStringSync(); + } + + final newContentNormalized = jsonString.replaceAll('\r\n', '\n'); + final oldContentNormalized = oldContent.replaceAll('\r\n', '\n'); + if (newContentNormalized != oldContentNormalized) { + file.writeAsStringSync(jsonString); + print('Generated $schemaUri (content changed)'); + counts.changed++; + } + counts.generated++; } } } -void generateEntryPoints() { +void generateEntryPoints(Counts counts) { for (final package in packages) { for (final hook in Hook.values) { if (hook == Hook.hook) continue; @@ -193,9 +234,26 @@ void generateEntryPoints() { }, ], }; - final jsonString = jsonEncoder.convert(contents); - File.fromUri(schemaUri).writeAsStringSync('$jsonString\n'); - print('Generated: $schemaUri'); + + var jsonString = jsonEncoder.convert( + sortJson(contents, schemaUri.path), + ); + jsonString += '\n'; + final file = File.fromUri(schemaUri); + + var oldContent = ''; + if (file.existsSync()) { + oldContent = file.readAsStringSync(); + } + + final newContentNormalized = jsonString.replaceAll('\r\n', '\n'); + final oldContentNormalized = oldContent.replaceAll('\r\n', '\n'); + if (newContentNormalized != oldContentNormalized) { + file.writeAsStringSync(jsonString); + print('Generated $schemaUri (content changed)'); + counts.changed++; + } + counts.generated++; } } } diff --git a/pkgs/hooks/tool/generate_syntax.dart b/pkgs/hooks/tool/generate_syntax.dart index 5cc31838fe..5054156f2a 100644 --- a/pkgs/hooks/tool/generate_syntax.dart +++ b/pkgs/hooks/tool/generate_syntax.dart @@ -6,6 +6,7 @@ import 'dart:io'; +import 'package:args/args.dart'; import 'package:json_syntax_generator/json_syntax_generator.dart'; import '../test/schema/helpers.dart'; @@ -19,6 +20,25 @@ final rootSchemas = loadSchemas([ ]); void main(List args) { + final stopwatch = Stopwatch()..start(); + final parser = ArgParser() + ..addFlag( + 'set-exit-if-changed', + negatable: false, + help: 'Return a non-zero exit code if any files were changed.', + ) + ..addFlag( + 'd', + negatable: false, + help: 'Dump analyzed schema to .g.txt file.', + ); + final argResults = parser.parse(args); + + final setExitIfChanged = argResults['set-exit-if-changed'] as bool; + final dumpAnalyzedSchema = argResults['d'] as bool; + var generatedCount = 0; + var changedCount = 0; + for (final packageName in generateFor) { const schemaName = 'shared'; final schemaUri = packageUri.resolve( @@ -48,11 +68,25 @@ void main(List args) { final textDumpFile = File.fromUri( packageUri.resolve('../$packageName/lib/src/$packageName/syntax.g.txt'), ); - if (args.contains('-d')) { - textDumpFile.writeAsStringSync(analyzedSchema.toString()); + + final newTextDumpContent = analyzedSchema.toString(); + if (dumpAnalyzedSchema) { + var oldTextDumpContent = ''; + if (textDumpFile.existsSync()) { + oldTextDumpContent = textDumpFile.readAsStringSync(); + } + if (oldTextDumpContent != newTextDumpContent) { + textDumpFile.writeAsStringSync(newTextDumpContent); + print('Generated ${textDumpFile.uri}'); + changedCount += 1; + } + generatedCount += 1; } else if (textDumpFile.existsSync()) { textDumpFile.deleteSync(); + print('Deleted ${textDumpFile.uri}'); + changedCount += 1; } + final output = SyntaxGenerator( analyzedSchema, header: @@ -65,9 +99,37 @@ void main(List args) { final outputUri = packageUri.resolve( '../$packageName/lib/src/$packageName/syntax.g.dart', ); - File.fromUri(outputUri).writeAsStringSync(output); + final outputFile = File.fromUri(outputUri); + var oldOutputContent = ''; + if (outputFile.existsSync()) { + oldOutputContent = outputFile.readAsStringSync(); + } + + if (oldOutputContent != output) { + outputFile.writeAsStringSync(output); + } + Process.runSync(Platform.executable, ['format', outputUri.toFilePath()]); - print('Generated $outputUri'); + + final newOutputContent = outputFile.readAsStringSync(); + + final newContentNormalized = newOutputContent.replaceAll('\r\n', '\n'); + final oldContentNormalized = oldOutputContent.replaceAll('\r\n', '\n'); + if (newContentNormalized != oldContentNormalized) { + print('Generated $outputUri'); + changedCount += 1; + } + generatedCount += 1; + } + + stopwatch.stop(); + final duration = stopwatch.elapsedMilliseconds / 1000.0; + print( + 'Generated $generatedCount files ($changedCount changed) in ' + '${duration.toStringAsFixed(2)} seconds.', + ); + if (setExitIfChanged && changedCount > 0) { + exit(1); } } diff --git a/pkgs/hooks/tool/normalize.dart b/pkgs/hooks/tool/normalize.dart index 284034a210..6f8f335f2b 100644 --- a/pkgs/hooks/tool/normalize.dart +++ b/pkgs/hooks/tool/normalize.dart @@ -4,11 +4,25 @@ import 'dart:convert'; import 'dart:io'; + +import 'package:args/args.dart'; import 'package:path/path.dart' as p; import '../test/schema/helpers.dart' show findPackageRoot; -void main() { +void main(List arguments) { + final stopwatch = Stopwatch()..start(); + final parser = ArgParser() + ..addFlag( + 'set-exit-if-changed', + negatable: false, + help: 'Return a non-zero exit code if any files were changed.', + ); + final argResults = parser.parse(arguments); + + final setExitIfChanged = argResults['set-exit-if-changed'] as bool; + var processedCount = 0; + var changedCount = 0; final packageUri = findPackageRoot('hooks'); final directories = [ @@ -17,32 +31,65 @@ void main() { Directory.fromUri(packageUri.resolve('../data_assets/')), ]; for (final directory in directories) { - processDirectory(directory); + final result = processDirectory(directory); + processedCount += result.processedCount; + changedCount += result.changedCount; + } + + stopwatch.stop(); + final duration = stopwatch.elapsedMilliseconds / 1000.0; + print( + 'Normalized $processedCount files ($changedCount changed) in ' + '${duration.toStringAsFixed(2)} seconds.', + ); + + if (setExitIfChanged && changedCount > 0) { + exit(1); } } -void processDirectory(Directory directory) { +class ProcessDirectoryResult { + final int processedCount; + final int changedCount; + + ProcessDirectoryResult(this.processedCount, this.changedCount); +} + +ProcessDirectoryResult processDirectory(Directory directory) { + var processedCount = 0; + var changedCount = 0; final entities = directory.listSync(recursive: true); for (final entity in entities) { if (entity is File && p.extension(entity.path) == '.json' && !entity.path.contains('.dart_tool/')) { - processFile(entity); + processedCount++; + if (processFile(entity)) { + changedCount += 1; + } } } + return ProcessDirectoryResult(processedCount, changedCount); } -void processFile(File file) { +bool processFile(File file) { final contents = file.readAsStringSync(); final dynamic decoded = json.decode(contents); - final sorted = sortJson(decoded, file.path); const encoder = JsonEncoder.withIndent(' '); - final sortedJson = encoder.convert(sorted); + final sortedJson = encoder.convert(sorted); // Already has no trailing newline + final newContents = '$sortedJson\n'; + + final newContentNormalized = newContents.replaceAll('\r\n', '\n'); + final oldContentNormalized = contents.replaceAll('\r\n', '\n'); + if (newContentNormalized == oldContentNormalized) { + return false; + } - file.writeAsStringSync('$sortedJson\n'); + file.writeAsStringSync(newContents); print('Normalized: ${file.path}'); + return true; } const List _orderedKeysInSchemas = [ @@ -192,12 +239,16 @@ int compareMaps(Map a, Map b) { final valueComparison = aValue.compareTo(bValue); if (valueComparison != 0) { return valueComparison; + } else { + continue; } } if (aValue is Map && bValue is Map) { final valueComparison = compareMaps(aValue, bValue); if (valueComparison != 0) { return valueComparison; + } else { + continue; } } if (aValue == bValue) { diff --git a/pkgs/hooks_runner/CHANGELOG.md b/pkgs/hooks_runner/CHANGELOG.md index ff29781a86..3ce687933e 100644 --- a/pkgs/hooks_runner/CHANGELOG.md +++ b/pkgs/hooks_runner/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.19.1-wip + +- Remove `package_graph.json` fallback. + ## 0.19.0 - Renamed package from `native_assets_builder` to `hooks_runner`. diff --git a/pkgs/hooks_runner/lib/src/build_runner/build_planner.dart b/pkgs/hooks_runner/lib/src/build_runner/build_planner.dart index 77d50b8d9b..bb3fd6b93e 100644 --- a/pkgs/hooks_runner/lib/src/build_runner/build_planner.dart +++ b/pkgs/hooks_runner/lib/src/build_runner/build_planner.dart @@ -3,7 +3,6 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:convert'; -import 'dart:io' show Process; import 'package:file/file.dart'; import 'package:graphs/graphs.dart' as graphs; @@ -39,21 +38,8 @@ class NativeAssetsBuildPlanner { final packageGraphJsonFile = fileSystem.file( packageConfigUri.resolve('package_graph.json'), ); - final String packageGraphJson; - if (packageGraphJsonFile.existsSync()) { - packageGraphJson = await packageGraphJsonFile.readAsString(); - } else { - // TODO: Either bump SDK constraint to dev release (but we can't while - // flutter_tools requires published stable packages). Or wait for Dart 3.8 - // to be released to remove this fallback. - final workingDirectory = packageConfigUri.resolve('../'); - final result = await Process.run( - dartExecutable.toFilePath(), - ['pub', 'deps', '--json'], - workingDirectory: workingDirectory.toFilePath(), - ); - packageGraphJson = result.stdout as String; - } + assert(packageGraphJsonFile.existsSync()); + final packageGraphJson = await packageGraphJsonFile.readAsString(); final packageGraph = PackageGraph.fromPackageGraphJsonString( packageGraphJson, ); diff --git a/pkgs/hooks_runner/pubspec.yaml b/pkgs/hooks_runner/pubspec.yaml index c929c9395d..65a5f46029 100644 --- a/pkgs/hooks_runner/pubspec.yaml +++ b/pkgs/hooks_runner/pubspec.yaml @@ -2,7 +2,7 @@ name: hooks_runner description: >- This package is the backend that invokes build hooks. -version: 0.19.0 +version: 0.19.1-wip repository: https://github.com/dart-lang/native/tree/main/pkgs/hooks_runner diff --git a/pkgs/hooks_runner/test/helpers.dart b/pkgs/hooks_runner/test/helpers.dart index 3a9c1151e4..bbcd247d0d 100644 --- a/pkgs/hooks_runner/test/helpers.dart +++ b/pkgs/hooks_runner/test/helpers.dart @@ -118,8 +118,8 @@ Future runProcess({ Uri findPackageRoot(String packageName) { final script = Platform.script; final fileName = script.name; - if (fileName.endsWith('_test.dart')) { - // We're likely running from source. + if (fileName.endsWith('.dart')) { + // We're likely running from source in the package somewhere. var directory = script.resolve('.'); while (true) { final dirName = directory.name; @@ -131,11 +131,23 @@ Uri findPackageRoot(String packageName) { directory = parent; } } else if (fileName.endsWith('.dill')) { + // Probably from the package root. final cwd = Directory.current.uri; final dirName = cwd.name; if (dirName == packageName) { return cwd; } + // Or the workspace root. + final candidate = cwd.resolve('pkgs/$packageName/'); + if (Directory.fromUri(candidate).existsSync()) { + return candidate; + } + } + // Or the workspace root. + final cwd = Directory.current.uri; + final candidate = cwd.resolve('pkgs/$packageName/'); + if (Directory.fromUri(candidate).existsSync()) { + return candidate; } throw StateError( "Could not find package root for package '$packageName'. " diff --git a/pkgs/hooks_runner/test_data/add_asset_link/pubspec.yaml b/pkgs/hooks_runner/test_data/add_asset_link/pubspec.yaml index 234de94d1f..9b91009975 100644 --- a/pkgs/hooks_runner/test_data/add_asset_link/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/add_asset_link/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: hooks: ^0.19.0 logging: ^1.3.0 meta: ^1.16.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: lints: ^5.1.1 diff --git a/pkgs/hooks_runner/test_data/drop_dylib_link/pubspec.yaml b/pkgs/hooks_runner/test_data/drop_dylib_link/pubspec.yaml index 9ef6af213f..ddd200984c 100644 --- a/pkgs/hooks_runner/test_data/drop_dylib_link/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/drop_dylib_link/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: lints: ^5.1.1 diff --git a/pkgs/hooks_runner/test_data/native_add/pubspec.yaml b/pkgs/hooks_runner/test_data/native_add/pubspec.yaml index 1c322fcb6c..2a3b505234 100644 --- a/pkgs/hooks_runner/test_data/native_add/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/native_add/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/native_add_add_source/pubspec.yaml b/pkgs/hooks_runner/test_data/native_add_add_source/pubspec.yaml index e6043d0a13..7c63d28a29 100644 --- a/pkgs/hooks_runner/test_data/native_add_add_source/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/native_add_add_source/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/native_add_duplicate/pubspec.yaml b/pkgs/hooks_runner/test_data/native_add_duplicate/pubspec.yaml index 045a7e2735..41ff60bde8 100644 --- a/pkgs/hooks_runner/test_data/native_add_duplicate/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/native_add_duplicate/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: logging: ^1.3.0 native_add: path: ../native_add/ - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/native_add_version_skew/analysis_options.yaml b/pkgs/hooks_runner/test_data/native_add_version_skew/analysis_options.yaml new file mode 100644 index 0000000000..349b9d631b --- /dev/null +++ b/pkgs/hooks_runner/test_data/native_add_version_skew/analysis_options.yaml @@ -0,0 +1,17 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + errors: + todo: ignore + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - dangling_library_doc_comments + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_in_for_each + - prefer_final_locals diff --git a/pkgs/hooks_runner/test_data/native_add_version_skew/pubspec.yaml b/pkgs/hooks_runner/test_data/native_add_version_skew/pubspec.yaml index fa094fae79..c9b2b47d50 100644 --- a/pkgs/hooks_runner/test_data/native_add_version_skew/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/native_add_version_skew/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: native_toolchain_c: ^0.12.0 dev_dependencies: + dart_flutter_team_lints: ^3.5.1 ffigen: ^18.0.0 lints: ^5.1.1 some_dev_dep: diff --git a/pkgs/hooks_runner/test_data/native_add_version_skew_2/analysis_options.yaml b/pkgs/hooks_runner/test_data/native_add_version_skew_2/analysis_options.yaml new file mode 100644 index 0000000000..349b9d631b --- /dev/null +++ b/pkgs/hooks_runner/test_data/native_add_version_skew_2/analysis_options.yaml @@ -0,0 +1,17 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + errors: + todo: ignore + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - dangling_library_doc_comments + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_in_for_each + - prefer_final_locals diff --git a/pkgs/hooks_runner/test_data/native_add_version_skew_2/pubspec.yaml b/pkgs/hooks_runner/test_data/native_add_version_skew_2/pubspec.yaml index c6b180be59..92353d100c 100644 --- a/pkgs/hooks_runner/test_data/native_add_version_skew_2/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/native_add_version_skew_2/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: native_toolchain_c: ^0.5.0 dev_dependencies: + dart_flutter_team_lints: ^3.5.1 ffigen: ^18.0.0 lints: ^5.1.1 some_dev_dep: diff --git a/pkgs/hooks_runner/test_data/native_dynamic_linking/pubspec.yaml b/pkgs/hooks_runner/test_data/native_dynamic_linking/pubspec.yaml index eb0c7ef8ef..e740e66be7 100644 --- a/pkgs/hooks_runner/test_data/native_dynamic_linking/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/native_dynamic_linking/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/native_subtract/pubspec.yaml b/pkgs/hooks_runner/test_data/native_subtract/pubspec.yaml index 8f583c9f2f..ffb9185f43 100644 --- a/pkgs/hooks_runner/test_data/native_subtract/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/native_subtract/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/no_hook/pubspec.yaml b/pkgs/hooks_runner/test_data/no_hook/pubspec.yaml index 8a84fd3192..5c3aef28e2 100644 --- a/pkgs/hooks_runner/test_data/no_hook/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/no_hook/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/reusable_dynamic_library/pubspec.yaml b/pkgs/hooks_runner/test_data/reusable_dynamic_library/pubspec.yaml index d224b4ebf4..2bd743c83e 100644 --- a/pkgs/hooks_runner/test_data/reusable_dynamic_library/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/reusable_dynamic_library/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/reuse_dynamic_library/pubspec.yaml b/pkgs/hooks_runner/test_data/reuse_dynamic_library/pubspec.yaml index 719a352ca8..ed59b4d0b5 100644 --- a/pkgs/hooks_runner/test_data/reuse_dynamic_library/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/reuse_dynamic_library/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip reusable_dynamic_library: path: ../reusable_dynamic_library/ diff --git a/pkgs/hooks_runner/test_data/system_library/pubspec.yaml b/pkgs/hooks_runner/test_data/system_library/pubspec.yaml index bf908a1e03..6a59616c48 100644 --- a/pkgs/hooks_runner/test_data/system_library/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/system_library/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/hooks_runner/test_data/treeshaking_native_libs/pubspec.yaml b/pkgs/hooks_runner/test_data/treeshaking_native_libs/pubspec.yaml index cdd428e5ca..8c0c43d6f1 100644 --- a/pkgs/hooks_runner/test_data/treeshaking_native_libs/pubspec.yaml +++ b/pkgs/hooks_runner/test_data/treeshaking_native_libs/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: code_assets: ^0.19.0 hooks: ^0.19.0 logging: ^1.3.0 - native_toolchain_c: ^0.16.0 + native_toolchain_c: ^0.16.1-wip dev_dependencies: ffigen: ^18.0.0 diff --git a/pkgs/jni/java/build.gradle.kts b/pkgs/jni/java/build.gradle.kts index 0f05b303df..ce27f25b49 100644 --- a/pkgs/jni/java/build.gradle.kts +++ b/pkgs/jni/java/build.gradle.kts @@ -9,8 +9,8 @@ repositories { } dependencies { - implementation(libs.org.jetbrains.kotlin.kotlin.stdlib) - implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.core) + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.22") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } group = "com.github.dart_lang" diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md index e8be9d5d73..a21b195674 100644 --- a/pkgs/jnigen/CHANGELOG.md +++ b/pkgs/jnigen/CHANGELOG.md @@ -9,6 +9,11 @@ instead of `build.gradle`. - Fixed a bug where Kotlin suspend functions that returned nullable values were generated incorrectly. +- Fixed a [bug](https://github.com/dart-lang/native/issues/2250) where classes + that inherited a generic without specifying all type parameters were not + generated. +- Added the ability to generate Kotlin stdlib classes without providing the + class path. ## 0.14.1 diff --git a/pkgs/jnigen/java/build.gradle.kts b/pkgs/jnigen/java/build.gradle.kts index 20a0f63e5c..0300bf8efa 100644 --- a/pkgs/jnigen/java/build.gradle.kts +++ b/pkgs/jnigen/java/build.gradle.kts @@ -10,11 +10,12 @@ repositories { } dependencies { - implementation(libs.com.fasterxml.jackson.core.jackson.databind) - implementation(libs.commons.cli.commons.cli) - implementation(libs.org.ow2.asm.asm.tree) - implementation(libs.org.jetbrains.kotlinx.kotlinx.metadata.jvm) - testImplementation(libs.junit.junit) + implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") + implementation("commons-cli:commons-cli:1.5.0") + implementation("org.ow2.asm:asm-tree:9.7") + implementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.9.0") + implementation(kotlin("stdlib-jdk8")) + testImplementation("junit:junit:4.13.2") } group = "com.github.dart_lang.jnigen" diff --git a/pkgs/jnigen/java/pom.xml b/pkgs/jnigen/java/pom.xml deleted file mode 100644 index 177b8c51af..0000000000 --- a/pkgs/jnigen/java/pom.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - 4.0.0 - - com.github.dart_lang.jnigen - placeholder - Placeholder pom.xml for CI setup. This is not used by the build. - 1.0-SNAPSHOT - - - 11 - 11 - UTF-8 - - - diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java index 718770db2f..733bd8e3d1 100644 --- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java +++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/Main.java @@ -8,7 +8,7 @@ import com.github.dart_lang.jnigen.apisummarizer.doclet.SummarizerDoclet; import com.github.dart_lang.jnigen.apisummarizer.elements.ClassDecl; import com.github.dart_lang.jnigen.apisummarizer.util.ClassFinder; -import com.github.dart_lang.jnigen.apisummarizer.util.JavaCoreClassFinder; +import com.github.dart_lang.jnigen.apisummarizer.util.CoreClassFinder; import com.github.dart_lang.jnigen.apisummarizer.util.JsonWriter; import com.github.dart_lang.jnigen.apisummarizer.util.StreamUtil; import java.io.*; @@ -101,7 +101,7 @@ public static void main(String[] args) throws FileNotFoundException { if (!foundBinary && !foundSource) { Map inputStreams = null; try { - inputStreams = JavaCoreClassFinder.findAll(qualifiedName); + inputStreams = CoreClassFinder.findAll(qualifiedName); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmAnnotationVisitor.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmAnnotationVisitor.java index 3fbf236b91..79619f088e 100644 --- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmAnnotationVisitor.java +++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/disasm/AsmAnnotationVisitor.java @@ -21,7 +21,11 @@ protected AsmAnnotationVisitor(JavaAnnotation annotation) { @Override public void visit(String name, Object value) { - annotation.properties.put(name, value); + if (value instanceof Number) { + annotation.properties.put(name, value); + } else { + annotation.properties.put(name, value.toString()); + } super.visit(name, value); } @@ -58,7 +62,11 @@ protected AnnotationArrayVisitor(List list) { @Override public void visit(String unused, Object value) { - list.add(value); + if (value instanceof Number) { + list.add(value); + } else { + list.add(value.toString()); + } super.visit(unused, value); } diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/CoreClassFinder.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/CoreClassFinder.java new file mode 100644 index 0000000000..f793b3678d --- /dev/null +++ b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/CoreClassFinder.java @@ -0,0 +1,303 @@ +// Copyright (c) 2025, 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. + +package com.github.dart_lang.jnigen.apisummarizer.util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Stream; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +public class CoreClassFinder { + + private static List findInnerClasses(InputStream inputStream, String fqcn) + throws IOException { + List innerClasses = new ArrayList<>(); + ClassReader classReader = new ClassReader(inputStream); + String internalName = fqcn.replace('.', '/'); + + classReader.accept( + new ClassVisitor(Opcodes.ASM9) { + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + if (outerName != null && outerName.equals(internalName)) { + innerClasses.add(name.replace('/', '.')); + } + super.visitInnerClass(name, outerName, innerName, access); + } + }, + 0); + return innerClasses; + } + + private static InputStream findJavaCoreClass(String className) { + String classResourcePath = "/" + className.replace('.', '/') + ".class"; + URI uri = URI.create("jrt:/"); + Map env = Collections.emptyMap(); + FileSystem fs = null; + boolean fsCreatedByUs = false; + try { + try { + fs = FileSystems.newFileSystem(uri, env); + fsCreatedByUs = true; + } catch (FileSystemAlreadyExistsException e) { + fs = FileSystems.getFileSystem(uri); + } + + Path path = fs.getPath("modules/java.base" + classResourcePath); + if (Files.notExists(path)) { + return null; + } + return Files.newInputStream(path); + } catch (IOException e) { + return null; + } finally { + if (fsCreatedByUs && fs != null) { + try { + fs.close(); + } catch (IOException ignored) { + } + } + } + } + + private static InputStream findKotlinStdlibClass(String className) { + String classResourcePath = className.replace('.', '/') + ".class"; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = CoreClassFinder.class.getClassLoader(); + } + if (classLoader == null) { + classLoader = ClassLoader.getSystemClassLoader(); + } + if (classLoader == null) { + return null; + } + return classLoader.getResourceAsStream(classResourcePath); + } + + private static void collectClassAndInnerClasses(String fqcn, Map targetMap) + throws IOException { + if (targetMap.containsKey(fqcn)) { // Avoid reprocessing if already added. + return; + } + + InputStream classInputStream = findJavaCoreClass(fqcn); + if (classInputStream == null) { + classInputStream = findKotlinStdlibClass(fqcn); + } + + if (classInputStream != null) { + byte[] mainClassBytes; + try { + mainClassBytes = classInputStream.readAllBytes(); + } finally { + try { + classInputStream.close(); + } catch (IOException ignored) { + } + } + targetMap.put(fqcn, new ByteArrayInputStream(mainClassBytes)); + + try { + List innerClassNames = + findInnerClasses(new ByteArrayInputStream(mainClassBytes), fqcn); + for (String innerClassName : innerClassNames) { + if (targetMap.containsKey(innerClassName)) continue; + + InputStream innerFinderStream = findJavaCoreClass(innerClassName); + if (innerFinderStream == null) { + innerFinderStream = findKotlinStdlibClass(innerClassName); + } + + if (innerFinderStream != null) { + byte[] innerClassBytes; + try { + innerClassBytes = innerFinderStream.readAllBytes(); + } finally { + try { + innerFinderStream.close(); + } catch (IOException ignored) { + } + } + targetMap.put(innerClassName, new ByteArrayInputStream(innerClassBytes)); + } + } + } catch (IOException ignored) { + } + } + } + + public static Map findAll(String name) throws IOException { + Map results = new HashMap<>(); + boolean nameIsLikelyFQCN = false; + + // Test if 'name' can be resolved as a class directly + InputStream testStream = findJavaCoreClass(name); + if (testStream != null) { + nameIsLikelyFQCN = true; + try { + testStream.close(); + } catch (IOException ignored) { + } + } else { + testStream = findKotlinStdlibClass(name); + if (testStream != null) { + nameIsLikelyFQCN = true; + try { + testStream.close(); + } catch (IOException ignored) { + } + } + } + + if (nameIsLikelyFQCN) { + // 'name' resolves to a class, so collect it and its inner classes. + collectClassAndInnerClasses(name, results); + } else { + // 'name' does not resolve to a class directly, treat it as a package name. + List topLevelClassNamesInPackage = new ArrayList<>(); + topLevelClassNamesInPackage.addAll(findTopLevelClassesInPackageJRT(name)); + topLevelClassNamesInPackage.addAll(findTopLevelClassesInPackageClasspath(name)); + + if (topLevelClassNamesInPackage.isEmpty()) { + // No specific class found for 'name', and no top-level classes found if 'name' is a + // package. + return null; + } + + for (String topLevelClassName : topLevelClassNamesInPackage) { + collectClassAndInnerClasses(topLevelClassName, results); + } + } + + return results.isEmpty() ? null : results; + } + + private static List findTopLevelClassesInPackageJRT(String packageName) { + List classNames = new ArrayList<>(); + String packageAsPath = "/" + packageName.replace('.', '/'); + URI uri = URI.create("jrt:/"); + Map env = Collections.emptyMap(); + FileSystem fs = null; + boolean fsCreatedByUs = false; + + try { + try { + fs = FileSystems.newFileSystem(uri, env); + fsCreatedByUs = true; + } catch (FileSystemAlreadyExistsException e) { + fs = FileSystems.getFileSystem(uri); + } + + Path dirPath = fs.getPath("modules/java.base" + packageAsPath); + if (Files.isDirectory(dirPath)) { + try (Stream stream = Files.list(dirPath)) { + stream + .filter( + path -> + path.getFileName().toString().endsWith(".class") + && !path.getFileName().toString().contains("$")) + .forEach( + path -> { + String fileName = path.getFileName().toString(); + String className = + packageName + + "." + + fileName.substring(0, fileName.length() - ".class".length()); + classNames.add(className); + }); + } + } + } catch (IOException ignored) { + } finally { + if (fsCreatedByUs && fs != null) { + try { + fs.close(); + } catch (IOException ignored) { + } + } + } + return classNames; + } + + private static List findTopLevelClassesInPackageClasspath(String packageName) { + List classNames = new ArrayList<>(); + String packagePath = packageName.replace('.', '/'); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) classLoader = CoreClassFinder.class.getClassLoader(); + if (classLoader == null) classLoader = ClassLoader.getSystemClassLoader(); + if (classLoader == null) return classNames; + + try { + Enumeration resources = classLoader.getResources(packagePath); + while (resources.hasMoreElements()) { + URL resourceUrl = resources.nextElement(); + if ("file".equals(resourceUrl.getProtocol())) { + try { + File directory = Paths.get(resourceUrl.toURI()).toFile(); + if (directory.isDirectory()) { + File[] files = + directory.listFiles( + (dir, name) -> name.endsWith(".class") && !name.contains("$")); + if (files != null) { + for (File file : files) { + String simpleClassName = + file.getName().substring(0, file.getName().length() - ".class".length()); + classNames.add(packageName + "." + simpleClassName); + } + } + } + } catch (URISyntaxException | SecurityException ignored) { + } + } else if ("jar".equals(resourceUrl.getProtocol())) { + String jarUrlPath = resourceUrl.getPath(); + String jarFilePath = jarUrlPath.substring("file:".length(), jarUrlPath.indexOf("!")); + try (JarFile jarFile = new JarFile(jarFilePath)) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (entryName.startsWith(packagePath + "/") + && entryName.endsWith(".class") + && !entryName.contains("$")) { + String namePart = + entryName.substring(packagePath.isEmpty() ? 0 : packagePath.length() + 1); + if (!namePart.contains("/")) { // Ensure it's directly in the package + String simpleClassName = + namePart.substring(0, namePart.length() - ".class".length()); + classNames.add(packageName + "." + simpleClassName); + } + } + } + } catch (IOException ignored) { + } + } + } + } catch (IOException ignored) { + } + return classNames; + } +} diff --git a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JavaCoreClassFinder.java b/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JavaCoreClassFinder.java deleted file mode 100644 index 1d16f40dd1..0000000000 --- a/pkgs/jnigen/java/src/main/java/com/github/dart_lang/jnigen/apisummarizer/util/JavaCoreClassFinder.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2025, 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. - -package com.github.dart_lang.jnigen.apisummarizer.util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.Opcodes; - -public class JavaCoreClassFinder { - private static List findInnerClasses(InputStream inputStream) throws IOException { - List innerClasses = new ArrayList<>(); - ClassReader classReader = new ClassReader(inputStream); - - classReader.accept( - new ClassVisitor(Opcodes.ASM9) { - @Override - public void visitInnerClass(String name, String outerName, String innerName, int access) { - innerClasses.add(name.replace('/', '.')); - super.visitInnerClass(name, outerName, innerName, access); - } - }, - 0); - - return innerClasses; - } - - private static InputStream find(String className) { - String classPath = "/" + className.replace('.', '/') + ".class"; - URI uri = URI.create("jrt:/"); - Map env = new HashMap<>(); - try (var fs = FileSystems.newFileSystem(uri, env)) { - Path path = fs.getPath("modules/java.base", classPath); - if (Files.notExists(path)) { - return null; - } - return Files.newInputStream(path); - } catch (IOException e) { - return null; - } - } - - /// Finds the class and all its inner classes. - public static Map findAll(String className) throws IOException { - var classes = new HashMap(); - var classInputStream = find(className); - if (classInputStream == null) { - return null; - } - var bytes = classInputStream.readAllBytes(); - classInputStream.close(); - classes.put(className, new ByteArrayInputStream(bytes)); - try { - var innerClasses = findInnerClasses(new ByteArrayInputStream(bytes)); - for (var innerClass : innerClasses) { - var innerClassInputStream = find(innerClass); - if (innerClassInputStream != null) { - classes.put(innerClass, innerClassInputStream); - } - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return classes; - } -} diff --git a/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/JSONComparisonTest.java b/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/JSONComparisonTest.java deleted file mode 100644 index b02ee15e56..0000000000 --- a/pkgs/jnigen/java/src/test/java/com/github/dart_lang/jnigen/apisummarizer/JSONComparisonTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.dart_lang.jnigen.apisummarizer; - -import com.github.dart_lang.jnigen.apisummarizer.util.Log; -import java.io.File; -import java.io.IOException; -import org.junit.Assert; -import org.junit.Test; - -public class JSONComparisonTest { - static final File exampleClassJsonOutput = - new File("src/test/resources/exampleClassSummary.json"); - - @SuppressWarnings("SameParameterValue") - private int gitDiff(File a, File b) throws IOException, InterruptedException { - return gitDiff(a.getPath(), b.getPath()); - } - - private int gitDiff(String a, String b) throws IOException, InterruptedException { - String colorSupport = "--color=never"; - if (System.console() != null && System.getenv().get("TERM") != null) { - colorSupport = "--color=always"; - } - String[] compareCommand = { - "git", "diff", colorSupport, "--no-index", a, b, - }; - var compare = Runtime.getRuntime().exec(compareCommand); - compare.getErrorStream().transferTo(System.err); - compare.getInputStream().transferTo(System.out); - return compare.waitFor(); - } - - @Test - public void testExampleSummary() throws IOException, InterruptedException { - var tempFile = File.createTempFile("summarizer_test", ".json"); - Log.info("Temporary file: %s", tempFile.getPath()); - Main.main( - new String[] { - "-s", "src/test/resources", "com.example.Example", "-o", tempFile.getPath(), - }); - int comparison = gitDiff(exampleClassJsonOutput, tempFile); - if (comparison != 0) { - Log.warning("New output (%s) is different than reference output.", tempFile.getPath()); - } - - // Fail test if git diff exited with 1 - Assert.assertEquals(0, comparison); - - var deleted = tempFile.delete(); - if (!deleted) { - Log.warning("Cannot delete temp file %s", tempFile.getPath()); - } - } -} diff --git a/pkgs/jnigen/java/src/test/resources/com/example/Example.java b/pkgs/jnigen/java/src/test/resources/com/example/Example.java deleted file mode 100644 index b5856a2777..0000000000 --- a/pkgs/jnigen/java/src/test/resources/com/example/Example.java +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2022, 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. - -package com.example; - -public class Example { - public static final boolean staticFinalField = true; - - public Example(int instanceField) { - this.instanceField = instanceField; - } - - public static String staticField = "hello"; - - public static String getStaticField() { - return staticField; - } - - public int instanceField; - - public int getInstanceField() { - return instanceField; - } - - protected int overrideableMethod(int x, int y) {} - - int defaultAccessNotVisible(); - - private int privateAccessNotVisible(); - - public static class Aux extends Example { - public static int nothing = 0; - - public static Example getAnExample() { - return new Example(); - } - } -} diff --git a/pkgs/jnigen/java/src/test/resources/exampleClassSummary.json b/pkgs/jnigen/java/src/test/resources/exampleClassSummary.json deleted file mode 100644 index d3163c477c..0000000000 --- a/pkgs/jnigen/java/src/test/resources/exampleClassSummary.json +++ /dev/null @@ -1,186 +0,0 @@ -[ { - "declKind" : "CLASS", - "modifiers" : [ "public" ], - "binaryName" : "com.example.Example", - "methods" : [ { - "modifiers" : [ "public" ], - "name" : "", - "params" : [ { - "name" : "instanceField", - "type" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - } ], - "returnType" : { - "shorthand" : "void", - "kind" : "PRIMITIVE", - "type" : { - "name" : "void" - } - } - }, { - "modifiers" : [ "static", "public" ], - "name" : "getStaticField", - "returnType" : { - "shorthand" : "java.lang.String", - "kind" : "DECLARED", - "type" : { - "binaryName" : "java.lang.String", - "simpleName" : "String" - } - } - }, { - "modifiers" : [ "public" ], - "name" : "getInstanceField", - "returnType" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - }, { - "modifiers" : [ "protected" ], - "name" : "overrideableMethod", - "params" : [ { - "name" : "x", - "type" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - }, { - "name" : "y", - "type" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - } ], - "returnType" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - }, { - "name" : "defaultAccessNotVisible", - "returnType" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - }, { - "modifiers" : [ "private" ], - "name" : "privateAccessNotVisible", - "returnType" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - } ], - "fields" : [ { - "modifiers" : [ "static", "public", "final" ], - "name" : "staticFinalField", - "type" : { - "shorthand" : "boolean", - "kind" : "PRIMITIVE", - "type" : { - "name" : "boolean" - } - }, - "defaultValue" : true - }, { - "modifiers" : [ "static", "public" ], - "name" : "staticField", - "type" : { - "shorthand" : "java.lang.String", - "kind" : "DECLARED", - "type" : { - "binaryName" : "java.lang.String", - "simpleName" : "String" - } - } - }, { - "modifiers" : [ "public" ], - "name" : "instanceField", - "type" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - } ], - "superclass" : { - "shorthand" : "java.lang.Object", - "kind" : "DECLARED", - "type" : { - "binaryName" : "java.lang.Object", - "simpleName" : "Object" - } - }, - "hasStaticInit" : false, - "hasInstanceInit" : false -}, { - "declKind" : "CLASS", - "modifiers" : [ "static", "public" ], - "binaryName" : "com.example.Example$Aux", - "methods" : [ { - "modifiers" : [ "public" ], - "name" : "", - "returnType" : { - "shorthand" : "void", - "kind" : "PRIMITIVE", - "type" : { - "name" : "void" - } - } - }, { - "modifiers" : [ "static", "public" ], - "name" : "getAnExample", - "returnType" : { - "shorthand" : "com.example.Example", - "kind" : "DECLARED", - "type" : { - "binaryName" : "com.example.Example", - "simpleName" : "Example" - } - } - } ], - "fields" : [ { - "modifiers" : [ "static", "public" ], - "name" : "nothing", - "type" : { - "shorthand" : "int", - "kind" : "PRIMITIVE", - "type" : { - "name" : "int" - } - } - } ], - "superclass" : { - "shorthand" : "com.example.Example", - "kind" : "DECLARED", - "type" : { - "binaryName" : "com.example.Example", - "simpleName" : "Example" - } - }, - "outerClassBinaryName" : "com.example.Example", - "hasStaticInit" : false, - "hasInstanceInit" : false -} ] \ No newline at end of file diff --git a/pkgs/jnigen/lib/src/bindings/linker.dart b/pkgs/jnigen/lib/src/bindings/linker.dart index 4133c337ff..512ee4607b 100644 --- a/pkgs/jnigen/lib/src/bindings/linker.dart +++ b/pkgs/jnigen/lib/src/bindings/linker.dart @@ -423,6 +423,9 @@ class _TypeMover extends TypeVisitor { final index = fromType.classDecl.allTypeParams .indexWhere((typeParam) => typeParam.name == typeVar.name); if (index != -1) { + if (index >= fromType.params.length) { + return DeclaredType.object.clone(); + } return fromType.params[index].clone(until: GenerationStage.linker); } } diff --git a/pkgs/jnigen/test/core_class_generation_test.dart b/pkgs/jnigen/test/core_class_generation_test.dart new file mode 100644 index 0000000000..0893bd95bd --- /dev/null +++ b/pkgs/jnigen/test/core_class_generation_test.dart @@ -0,0 +1,62 @@ +// Copyright (c) 2025, 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:jnigen/src/config/config_types.dart'; +import 'package:test/test.dart'; + +import 'test_util/test_util.dart'; + +void main() { + test('Java core libraries are generated without providing class path', + () async { + await generateAndAnalyzeBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: Uri.file('foo.dart'), + structure: OutputStructure.singleFile, + ), + ), + classes: [ + // A random assortment of Java core classes. + 'java.lang.StringBuilder', + 'java.lang.ModuleLayer', + 'java.net.SocketOption', + 'java.lang.ref', // Also works with packages. + ], + ), + confirmExists: [ + 'StringBuilder', + 'ModuleLayer', + 'SocketOption', + 'Reference', // From `java.lang.ref`. + ], + ); + }); + + test('Kotlin stdlib libraries are generated without providing class path', + () async { + await generateAndAnalyzeBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: Uri.file('foo.dart'), + structure: OutputStructure.singleFile, + ), + ), + classes: [ + // A random assortment of Kotlin stdlib classes. + 'kotlin.io.AccessDeniedException', + 'kotlin.ranges.CharRange', + 'kotlin.random', // Also works with packages. + ], + ), + confirmExists: [ + 'AccessDeniedException', + 'CharRange', + 'Random', // From `kotlin.random`. + ], + ); + }); +} diff --git a/pkgs/jnigen/test/jackson_core_test/generate.dart b/pkgs/jnigen/test/jackson_core_test/generate.dart index e7653df224..eebf42cabe 100644 --- a/pkgs/jnigen/test/jackson_core_test/generate.dart +++ b/pkgs/jnigen/test/jackson_core_test/generate.dart @@ -57,19 +57,6 @@ Config getConfig({ 'com.fasterxml.jackson.core.JsonToken', ], logLevel: Level.INFO, - exclude: BindingExclusions( - // TODO(#31): Remove field exclusions. - fields: excludeAll([ - ['com.fasterxml.jackson.core.JsonFactory', 'DEFAULT_QUOTE_CHAR'], - ['com.fasterxml.jackson.core.Base64Variant', 'PADDING_CHAR_NONE'], - ['com.fasterxml.jackson.core.base.ParserMinimalBase', 'CHAR_NULL'], - ['com.fasterxml.jackson.core.io.UTF32Reader', 'NC'], - ]), - // TODO(#159): Remove class exclusions. - classes: ClassNameFilter.exclude( - 'com.fasterxml.jackson.core.JsonFactoryBuilder', - ), - ), ); return config; } diff --git a/pkgs/jnigen/test/java_core_generation_test.dart b/pkgs/jnigen/test/java_core_generation_test.dart deleted file mode 100644 index 2a3948941a..0000000000 --- a/pkgs/jnigen/test/java_core_generation_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2025, 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:jnigen/src/config/config_types.dart'; -import 'package:test/test.dart'; - -import 'test_util/test_util.dart'; - -void main() { - test('Java core libraries are generated without providing class path', - () async { - await generateAndAnalyzeBindings( - Config( - outputConfig: OutputConfig( - dartConfig: DartCodeOutputConfig( - path: Uri.file('foo.dart'), - structure: OutputStructure.singleFile, - ), - ), - classes: [ - // A random assortment of Java core classes. - 'java.lang.StringBuilder', - 'java.lang.ModuleLayer', - 'java.net.SocketOption', - ], - ), - confirmExists: ['StringBuilder', 'ModuleLayer', 'SocketOption'], - ); - }); -} diff --git a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart index 89a41fc91f..660e76d219 100644 --- a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart +++ b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart @@ -13246,3 +13246,760 @@ final class $Nullable$Type extends jni$_.JObjType { return other.runtimeType == ($Nullable$Type) && other is $Nullable$Type; } } + +/// from: `com.github.dart_lang.jnigen.regressions.R2250$Child` +class R2250$Child extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + R2250$Child.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'com/github/dart_lang/jnigen/regressions/R2250$Child'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $R2250$Child$NullableType(); + static const type = $R2250$Child$Type(); + static final _id_foo = _class.instanceMethodId( + r'foo', + r'(Ljava/lang/Object;)V', + ); + + static final _foo = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public abstract void foo(java.lang.Object object)` + void foo( + jni$_.JObject? object, + ) { + final _$object = object?.reference ?? jni$_.jNullReference; + _foo(reference.pointer, _id_foo as jni$_.JMethodIDPtr, _$object.pointer) + .check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses( + 0, + descriptor.address, + args.address, + ), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function( + jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr)>> + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'foo(Ljava/lang/Object;)V') { + _$impls[$p]!.foo( + $a![0]?.as(const jni$_.JObjectType(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $R2250$Child $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'com.github.dart_lang.jnigen.regressions.R2250$Child', + $p, + _$invokePointer, + [ + if ($impl.foo$async) r'foo(Ljava/lang/Object;)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory R2250$Child.implement( + $R2250$Child $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return R2250$Child.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $R2250$Child { + factory $R2250$Child({ + required void Function(jni$_.JObject? object) foo, + bool foo$async, + }) = _$R2250$Child; + + void foo(jni$_.JObject? object); + bool get foo$async => false; +} + +final class _$R2250$Child with $R2250$Child { + _$R2250$Child({ + required void Function(jni$_.JObject? object) foo, + this.foo$async = false, + }) : _foo = foo; + + final void Function(jni$_.JObject? object) _foo; + final bool foo$async; + + void foo(jni$_.JObject? object) { + return _foo(object); + } +} + +final class $R2250$Child$NullableType extends jni$_.JObjType { + @jni$_.internal + const $R2250$Child$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/github/dart_lang/jnigen/regressions/R2250$Child;'; + + @jni$_.internal + @core$_.override + R2250$Child? fromReference(jni$_.JReference reference) => reference.isNull + ? null + : R2250$Child.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($R2250$Child$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R2250$Child$NullableType) && + other is $R2250$Child$NullableType; + } +} + +final class $R2250$Child$Type extends jni$_.JObjType { + @jni$_.internal + const $R2250$Child$Type(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/github/dart_lang/jnigen/regressions/R2250$Child;'; + + @jni$_.internal + @core$_.override + R2250$Child fromReference(jni$_.JReference reference) => + R2250$Child.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $R2250$Child$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($R2250$Child$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R2250$Child$Type) && + other is $R2250$Child$Type; + } +} + +/// from: `com.github.dart_lang.jnigen.regressions.R2250` +class R2250<$T extends jni$_.JObject?> extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType> $type; + + @jni$_.internal + final jni$_.JObjType<$T> T; + + @jni$_.internal + R2250.fromReference( + this.T, + jni$_.JReference reference, + ) : $type = type<$T>(T), + super.fromReference(reference); + + static final _class = + jni$_.JClass.forName(r'com/github/dart_lang/jnigen/regressions/R2250'); + + /// The type which includes information such as the signature of this class. + static $R2250$NullableType<$T> nullableType<$T extends jni$_.JObject?>( + jni$_.JObjType<$T> T, + ) { + return $R2250$NullableType<$T>( + T, + ); + } + + static $R2250$Type<$T> type<$T extends jni$_.JObject?>( + jni$_.JObjType<$T> T, + ) { + return $R2250$Type<$T>( + T, + ); + } + + static final _id_foo = _class.instanceMethodId( + r'foo', + r'(Ljava/lang/Object;)V', + ); + + static final _foo = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>)>>( + 'globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.Pointer)>(); + + /// from: `public abstract void foo(T object)` + void foo( + $T? object, + ) { + final _$object = object?.reference ?? jni$_.jNullReference; + _foo(reference.pointer, _id_foo as jni$_.JMethodIDPtr, _$object.pointer) + .check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = {}; + static jni$_.JObjectPtr _$invoke( + int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses( + 0, + descriptor.address, + args.address, + ), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function( + jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr)>> + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'foo(Ljava/lang/Object;)V') { + _$impls[$p]!.foo( + $a![0]?.as(_$impls[$p]!.T, releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn<$T extends jni$_.JObject?>( + jni$_.JImplementer implementer, + $R2250<$T> $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'com.github.dart_lang.jnigen.regressions.R2250', + $p, + _$invokePointer, + [ + if ($impl.foo$async) r'foo(Ljava/lang/Object;)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory R2250.implement( + $R2250<$T> $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return R2250<$T>.fromReference( + $impl.T, + $i.implementReference(), + ); + } +} + +abstract base mixin class $R2250<$T extends jni$_.JObject?> { + factory $R2250({ + required jni$_.JObjType<$T> T, + required void Function($T? object) foo, + bool foo$async, + }) = _$R2250<$T>; + + jni$_.JObjType<$T> get T; + + void foo($T? object); + bool get foo$async => false; +} + +final class _$R2250<$T extends jni$_.JObject?> with $R2250<$T> { + _$R2250({ + required this.T, + required void Function($T? object) foo, + this.foo$async = false, + }) : _foo = foo; + + @core$_.override + final jni$_.JObjType<$T> T; + + final void Function($T? object) _foo; + final bool foo$async; + + void foo($T? object) { + return _foo(object); + } +} + +final class $R2250$NullableType<$T extends jni$_.JObject?> + extends jni$_.JObjType?> { + @jni$_.internal + final jni$_.JObjType<$T> T; + + @jni$_.internal + const $R2250$NullableType( + this.T, + ); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jnigen/regressions/R2250;'; + + @jni$_.internal + @core$_.override + R2250<$T>? fromReference(jni$_.JReference reference) => reference.isNull + ? null + : R2250<$T>.fromReference( + T, + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType?> get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($R2250$NullableType, T); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R2250$NullableType<$T>) && + other is $R2250$NullableType<$T> && + T == other.T; + } +} + +final class $R2250$Type<$T extends jni$_.JObject?> + extends jni$_.JObjType> { + @jni$_.internal + final jni$_.JObjType<$T> T; + + @jni$_.internal + const $R2250$Type( + this.T, + ); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jnigen/regressions/R2250;'; + + @jni$_.internal + @core$_.override + R2250<$T> fromReference(jni$_.JReference reference) => + R2250<$T>.fromReference( + T, + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType?> get nullableType => $R2250$NullableType<$T>(T); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($R2250$Type, T); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R2250$Type<$T>) && + other is $R2250$Type<$T> && + T == other.T; + } +} + +/// from: `com.github.dart_lang.jnigen.regressions.R693$Child` +class R693$Child extends R693 { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + R693$Child.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(const $R693$Child$NullableType(), reference); + + static final _class = jni$_.JClass.forName( + r'com/github/dart_lang/jnigen/regressions/R693$Child'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $R693$Child$NullableType(); + static const type = $R693$Child$Type(); + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory R693$Child() { + return R693$Child.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr) + .reference); + } +} + +final class $R693$Child$NullableType extends jni$_.JObjType { + @jni$_.internal + const $R693$Child$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/github/dart_lang/jnigen/regressions/R693$Child;'; + + @jni$_.internal + @core$_.override + R693$Child? fromReference(jni$_.JReference reference) => reference.isNull + ? null + : R693$Child.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => + const $R693$NullableType($R693$Child$NullableType()); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 2; + + @core$_.override + int get hashCode => ($R693$Child$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R693$Child$NullableType) && + other is $R693$Child$NullableType; + } +} + +final class $R693$Child$Type extends jni$_.JObjType { + @jni$_.internal + const $R693$Child$Type(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/github/dart_lang/jnigen/regressions/R693$Child;'; + + @jni$_.internal + @core$_.override + R693$Child fromReference(jni$_.JReference reference) => + R693$Child.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => + const $R693$NullableType($R693$Child$NullableType()); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $R693$Child$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 2; + + @core$_.override + int get hashCode => ($R693$Child$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R693$Child$Type) && other is $R693$Child$Type; + } +} + +/// from: `com.github.dart_lang.jnigen.regressions.R693` +class R693<$T extends jni$_.JObject?> extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType> $type; + + @jni$_.internal + final jni$_.JObjType<$T> T; + + @jni$_.internal + R693.fromReference( + this.T, + jni$_.JReference reference, + ) : $type = type<$T>(T), + super.fromReference(reference); + + static final _class = + jni$_.JClass.forName(r'com/github/dart_lang/jnigen/regressions/R693'); + + /// The type which includes information such as the signature of this class. + static $R693$NullableType<$T> nullableType<$T extends jni$_.JObject?>( + jni$_.JObjType<$T> T, + ) { + return $R693$NullableType<$T>( + T, + ); + } + + static $R693$Type<$T> type<$T extends jni$_.JObject?>( + jni$_.JObjType<$T> T, + ) { + return $R693$Type<$T>( + T, + ); + } + + static final _id_new$ = _class.constructorId( + r'()V', + ); + + static final _new$ = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>>('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + )>(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory R693({ + required jni$_.JObjType<$T> T, + }) { + return R693<$T>.fromReference( + T, + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr) + .reference); + } +} + +final class $R693$NullableType<$T extends jni$_.JObject?> + extends jni$_.JObjType?> { + @jni$_.internal + final jni$_.JObjType<$T> T; + + @jni$_.internal + const $R693$NullableType( + this.T, + ); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jnigen/regressions/R693;'; + + @jni$_.internal + @core$_.override + R693<$T>? fromReference(jni$_.JReference reference) => reference.isNull + ? null + : R693<$T>.fromReference( + T, + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType?> get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($R693$NullableType, T); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R693$NullableType<$T>) && + other is $R693$NullableType<$T> && + T == other.T; + } +} + +final class $R693$Type<$T extends jni$_.JObject?> + extends jni$_.JObjType> { + @jni$_.internal + final jni$_.JObjType<$T> T; + + @jni$_.internal + const $R693$Type( + this.T, + ); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jnigen/regressions/R693;'; + + @jni$_.internal + @core$_.override + R693<$T> fromReference(jni$_.JReference reference) => R693<$T>.fromReference( + T, + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType?> get nullableType => $R693$NullableType<$T>(T); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => Object.hash($R693$Type, T); + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($R693$Type<$T>) && + other is $R693$Type<$T> && + T == other.T; + } +} diff --git a/pkgs/jnigen/test/simple_package_test/generate.dart b/pkgs/jnigen/test/simple_package_test/generate.dart index 4921ebb013..5cfcfe83ba 100644 --- a/pkgs/jnigen/test/simple_package_test/generate.dart +++ b/pkgs/jnigen/test/simple_package_test/generate.dart @@ -28,11 +28,6 @@ final javaFiles = [ join(javaPrefix, 'annotations', 'MyDataClass.java'), join(javaPrefix, 'annotations', 'NotNull.java'), join(javaPrefix, 'annotations', 'Nullable.java'), - join(javaPrefix, 'simple_package', 'Example.java'), - join(javaPrefix, 'simple_package', 'Exceptions.java'), - join(javaPrefix, 'simple_package', 'Fields.java'), - join(javaPrefix, 'pkg2', 'C2.java'), - join(javaPrefix, 'pkg2', 'Example.java'), join(javaPrefix, 'enums', 'Colors.java'), join(javaPrefix, 'generics', 'MyStack.java'), join(javaPrefix, 'generics', 'MyMap.java'), @@ -58,6 +53,13 @@ final javaFiles = [ join(javaPrefix, 'interfaces', 'StringConversionException.java'), join(javaPrefix, 'interfaces', 'StringConverter.java'), join(javaPrefix, 'interfaces', 'StringConverterConsumer.java'), + join(javaPrefix, 'pkg2', 'C2.java'), + join(javaPrefix, 'pkg2', 'Example.java'), + join(javaPrefix, 'regressions', 'R693.java'), + join(javaPrefix, 'regressions', 'R2250.java'), + join(javaPrefix, 'simple_package', 'Example.java'), + join(javaPrefix, 'simple_package', 'Exceptions.java'), + join(javaPrefix, 'simple_package', 'Fields.java'), ]; void compileJavaSources(String workingDir, List files) async { @@ -85,6 +87,7 @@ Config getConfig({SummarizerBackend backend = SummarizerBackend.asm}) { 'com.github.dart_lang.jnigen.interfaces', 'com.github.dart_lang.jnigen.inheritance', 'com.github.dart_lang.jnigen.annotations', + 'com.github.dart_lang.jnigen.regressions', ], logLevel: Level.INFO, nonNullAnnotations: ['com.github.dart_lang.jnigen.annotations.NotNull'], diff --git a/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/regressions/R2250.java b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/regressions/R2250.java new file mode 100644 index 0000000000..fe2f831536 --- /dev/null +++ b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/regressions/R2250.java @@ -0,0 +1,12 @@ +// Copyright (c) 2025, 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. + +package com.github.dart_lang.jnigen.regressions; + +// Regression test for https://github.com/dart-lang/native/issues/2250. +public interface R2250 { + public void foo(T t); + + public interface Child extends R2250 {} +} diff --git a/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/regressions/R693.java b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/regressions/R693.java new file mode 100644 index 0000000000..14caa187f1 --- /dev/null +++ b/pkgs/jnigen/test/simple_package_test/java/com/github/dart_lang/jnigen/regressions/R693.java @@ -0,0 +1,18 @@ +// Copyright (c) 2025, 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. + +package com.github.dart_lang.jnigen.regressions; + +// Regression test for https://github.com/dart-lang/native/issues/693. +public class R693> { + T foo() { + return null; + } + + public static class Child extends R693 { + Child foo() { + return new Child(); + } + } +} diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index 7cf9a331d8..770190813a 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -978,6 +978,23 @@ void registerTests(String groupName, TestRunnerCallback test) { ); }); }); + test('Superinterface methods are available', () { + expect( + $R2250.new, + isA< + $R2250<$T> Function<$T extends JObject?>( + // ignore: invalid_use_of_internal_member + {required JObjType<$T> T, + required void Function($T?) foo, + bool foo$async})>(), + ); + expect( + $R2250$Child.new, + isA< + $R2250$Child Function( + {required void Function(JObject?) foo, bool foo$async})>(), + ); + }); }); group('Nullablity annotations', () { diff --git a/pkgs/native_toolchain_c/CHANGELOG.md b/pkgs/native_toolchain_c/CHANGELOG.md index 0e9e08ca81..93b8e75496 100644 --- a/pkgs/native_toolchain_c/CHANGELOG.md +++ b/pkgs/native_toolchain_c/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.16.1-wip + +- Firebase Studio NixOS support (default install locations for native + toolchains). + ## 0.16.0 - Depend on `package:code_assets` and `package:hooks` 0.19.0. diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart index 8ba2d719ea..5eaad2ba0c 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart @@ -51,8 +51,8 @@ class CompilerResolver { final targetOS = codeConfig.targetOS; final targetArchitecture = codeConfig.targetArchitecture; final errorMessage = - "No tools configured on host '${hostOS}_$hostArchitecture' with target " - "'${targetOS}_$targetArchitecture'."; + "No compiler configured on host '${hostOS}_$hostArchitecture' with " + "target '${targetOS}_$targetArchitecture'."; logger?.severe(errorMessage); throw ToolError(errorMessage); } @@ -141,8 +141,8 @@ class CompilerResolver { final targetOS = codeConfig.targetOS; final targetArchitecture = codeConfig.targetArchitecture; final errorMessage = - "No tools configured on host '${hostOS}_$hostArchitecture' with target " - "'${targetOS}_$targetArchitecture'."; + "No archiver configured on host '${hostOS}_$hostArchitecture' with " + "target '${targetOS}_$targetArchitecture'."; logger?.severe(errorMessage); throw ToolError(errorMessage); } @@ -259,8 +259,8 @@ class CompilerResolver { } final errorMessage = - "No tools configured on host '${hostOS}_$hostArchitecture' with target " - "'${targetOS}_$targetArchitecture'."; + "No linker configured on host '${hostOS}_$hostArchitecture' with " + "target '${targetOS}_$targetArchitecture'."; logger?.severe(errorMessage); throw ToolError(errorMessage); } @@ -288,6 +288,9 @@ class CompilerResolver { if (targetOS == OS.macOS || targetOS == OS.iOS) return appleLd; if (targetOS == OS.android) return androidNdkLld; if (hostOS == OS.linux) { + if (Architecture.current == targetArchitecture) { + return lld; + } switch (targetArchitecture) { case Architecture.arm: return armLinuxGnueabihfLd; 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 index 4539b51344..171b2a1c11 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart @@ -50,6 +50,7 @@ class _AndroidNdkResolver implements ToolResolver { toolName: 'Android NDK', paths: [ if (Platform.isLinux) ...[ + '\$HOME/.androidsdkroot/ndk/*/', // Firebase Studio '\$HOME/Android/Sdk/ndk/*/', '\$HOME/Android/Sdk/ndk-bundle/', ], diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/clang.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/clang.dart index 4c3f3fbd7e..ff08f5175e 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/clang.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/clang.dart @@ -48,6 +48,10 @@ final Tool llvmAr = Tool( wrappedResolver: clang.defaultResolver!, relativePath: Uri.file(OS.current.executableFileName('llvm-ar')), ), + PathToolResolver( + toolName: 'LLVM archiver', + executableName: OS.current.executableFileName('llvm-ar'), + ), ]), ), ); @@ -64,6 +68,10 @@ final Tool lld = Tool( wrappedResolver: clang.defaultResolver!, relativePath: Uri.file(OS.current.executableFileName('ld.lld')), ), + PathToolResolver( + toolName: 'LLD', + executableName: OS.current.executableFileName('ld.lld'), + ), ]), ), ); diff --git a/pkgs/native_toolchain_c/pubspec.yaml b/pkgs/native_toolchain_c/pubspec.yaml index 59c902919f..6d124234be 100644 --- a/pkgs/native_toolchain_c/pubspec.yaml +++ b/pkgs/native_toolchain_c/pubspec.yaml @@ -1,7 +1,7 @@ name: native_toolchain_c description: >- A library to invoke the native C compiler installed on the host machine. -version: 0.16.0 +version: 0.16.1-wip repository: https://github.com/dart-lang/native/tree/main/pkgs/native_toolchain_c topics: diff --git a/pkgs/native_toolchain_c/test/helpers.dart b/pkgs/native_toolchain_c/test/helpers.dart index 166828c9ab..9c1b0338de 100644 --- a/pkgs/native_toolchain_c/test/helpers.dart +++ b/pkgs/native_toolchain_c/test/helpers.dart @@ -91,8 +91,8 @@ Logger _createTestLogger({List? capturedMessages}) => Uri findPackageRoot(String packageName) { final script = Platform.script; final fileName = script.name; - if (fileName.endsWith('_test.dart')) { - // We're likely running from source. + if (fileName.endsWith('.dart')) { + // We're likely running from source in the package somewhere. var directory = script.resolve('.'); while (true) { final dirName = directory.name; @@ -104,12 +104,19 @@ Uri findPackageRoot(String packageName) { directory = parent; } } else if (fileName.endsWith('.dill')) { + // Probably from the package root. final cwd = Directory.current.uri; final dirName = cwd.name; if (dirName == packageName) { return cwd; } } + // Or the workspace root. + final cwd = Directory.current.uri; + final candidate = cwd.resolve('pkgs/$packageName/'); + if (Directory.fromUri(candidate).existsSync()) { + return candidate; + } throw StateError( "Could not find package root for package '$packageName'. " 'Tried finding the package root via Platform.script ' diff --git a/pkgs/native_toolchain_c/test/native_toolchain/ndk_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/ndk_test.dart index f6cde28e31..b21b0aa976 100644 --- a/pkgs/native_toolchain_c/test/native_toolchain/ndk_test.dart +++ b/pkgs/native_toolchain_c/test/native_toolchain/ndk_test.dart @@ -17,6 +17,7 @@ void main() { ToolRequirement(androidNdkLld), ]); final resolved = await androidNdk.defaultResolver!.resolve(logger: logger); + printOnFailure(resolved.toString()); final satisfied = requirement.satisfy(resolved); expect(satisfied?.length, 4); }); diff --git a/pkgs/objective_c/CHANGELOG.md b/pkgs/objective_c/CHANGELOG.md index 4688eec649..16fa45209e 100644 --- a/pkgs/objective_c/CHANGELOG.md +++ b/pkgs/objective_c/CHANGELOG.md @@ -2,7 +2,10 @@ - Use ffigen 18.2.0 - `NSArray` is now a Dart `Iterable` and `NSMutableArray` is now a Dart `List`. +- `NSDictionary` and `NSMutableDictionary` are now Dart `Map`s. +- `NSSet` and `NSMutableSet` are now Dart `Set`s. - Add `.toNSNumber()` extension method to `int`, `double`, and `num`. +- Add `DateTime.toNSDate()` and `NSDate.toDateTime()` extension methods. ## 7.1.0 diff --git a/pkgs/objective_c/lib/objective_c.dart b/pkgs/objective_c/lib/objective_c.dart index 65fb17248f..02e0806a77 100644 --- a/pkgs/objective_c/lib/objective_c.dart +++ b/pkgs/objective_c/lib/objective_c.dart @@ -21,6 +21,7 @@ export 'src/c_bindings_generated.dart' export 'src/internal.dart' hide blockHasRegisteredClosure, isValidBlock, isValidClass, isValidObject; export 'src/ns_data.dart'; +export 'src/ns_date.dart'; export 'src/ns_input_stream.dart'; export 'src/ns_mutable_data.dart'; export 'src/ns_number.dart'; diff --git a/pkgs/objective_c/lib/src/ns_date.dart b/pkgs/objective_c/lib/src/ns_date.dart new file mode 100644 index 0000000000..1421f8f153 --- /dev/null +++ b/pkgs/objective_c/lib/src/ns_date.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2025, 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 'objective_c_bindings_generated.dart'; + +extension DateTimeToNSDate on DateTime { + NSDate toNSDate() => NSDate.dateWithTimeIntervalSince1970_( + millisecondsSinceEpoch / Duration.millisecondsPerSecond); +} + +extension NSDateToDateTime on NSDate { + DateTime toDateTime() => DateTime.fromMillisecondsSinceEpoch( + (timeIntervalSince1970 * Duration.millisecondsPerSecond).toInt()); +} diff --git a/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart b/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart index fa90ccea34..4428f3f9b8 100644 --- a/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart +++ b/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart @@ -2022,7 +2022,39 @@ class NSDate extends NSObject implements NSCopying, NSSecureCoding { /// NSDictionary class NSDictionary extends NSObject + with MapBase implements NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration { + /// Creates a [NSDictionary] from [other]. + static NSDictionary of(Map other) => + NSMutableDictionary.of(other); + + @override + int get length => count; + + @override + objc.ObjCObjectBase? operator [](Object? key) => + key is NSCopying ? objectForKey_(key) : null; + + @override + Iterable get keys => _NSDictionaryKeyIterable(this); + + @override + Iterable get values => _NSDictionaryValueIterable(this); + + @override + bool containsKey(Object? key) => this[key] != null; + + @override + void operator []=(NSCopying key, objc.ObjCObjectBase value) => + throw UnsupportedError("Cannot modify NSDictionary"); + + @override + void clear() => throw UnsupportedError("Cannot modify NSDictionary"); + + @override + objc.ObjCObjectBase? remove(Object? key) => + throw UnsupportedError("Cannot modify NSDictionary"); + NSDictionary._(ffi.Pointer pointer, {bool retain = false, bool release = false}) : super.castFromPointer(pointer, retain: retain, release: release); @@ -2228,7 +2260,20 @@ enum NSEnumerationOptions { } /// NSEnumerator -class NSEnumerator extends NSObject implements NSFastEnumeration { +class NSEnumerator extends NSObject + implements NSFastEnumeration, Iterator { + objc.ObjCObjectBase? _current; + + @override + objc.ObjCObjectBase get current => _current!; + + @override + @pragma('vm:prefer-inline') + bool moveNext() { + _current = nextObject(); + return _current != null; + } + NSEnumerator._(ffi.Pointer pointer, {bool retain = false, bool release = false}) : super.castFromPointer(pointer, retain: retain, release: release); @@ -4694,7 +4739,7 @@ class NSMethodSignature extends NSObject { } /// NSMutableArray -class NSMutableArray extends NSArray with ListMixin { +class NSMutableArray extends NSArray with ListBase { /// Creates a [NSMutableArray] of the given length with [fill] at each /// position. /// @@ -4716,6 +4761,12 @@ class NSMutableArray extends NSArray with ListMixin { for (; len > newLength; --len) removeLastObject(); } + @override + Iterator get iterator => _NSArrayIterator(this); + + @override + objc.ObjCObjectBase operator [](int index) => objectAtIndex_(index); + @override void operator []=(int index, objc.ObjCObjectBase value) => replaceObjectAtIndex_withObject_(index, value); @@ -4723,11 +4774,6 @@ class NSMutableArray extends NSArray with ListMixin { @override void add(objc.ObjCObjectBase value) => addObject_(value); - @override - void addAll(Iterable iterable) { - for (final value in iterable) add(value); - } - NSMutableArray._(ffi.Pointer pointer, {bool retain = false, bool release = false}) : super.castFromPointer(pointer, retain: retain, release: release); @@ -5339,6 +5385,25 @@ class NSMutableData extends NSData { /// NSMutableDictionary class NSMutableDictionary extends NSDictionary { + /// Creates a [NSMutableDictionary] from [other]. + static NSDictionary of(Map other) => + NSMutableDictionary.dictionaryWithCapacity_(other.length)..addAll(other); + + @override + void clear() => removeAllObjects(); + + @override + objc.ObjCObjectBase? remove(Object? key) { + if (key is! NSCopying) return null; + final old = this[key]; + removeObjectForKey_(key); + return old; + } + + @override + void operator []=(NSCopying key, objc.ObjCObjectBase value) => + setObject_forKey_(value, NSCopying.castFrom(key)); + NSMutableDictionary._(ffi.Pointer pointer, {bool retain = false, bool release = false}) : super.castFromPointer(pointer, retain: retain, release: release); @@ -6083,6 +6148,28 @@ class NSMutableOrderedSet extends NSOrderedSet { /// NSMutableSet class NSMutableSet extends NSSet { + /// Creates a [NSMutableSet] from [elements]. + static NSMutableSet of(Iterable elements) => + setWithCapacity_(elements.length)..addAll(elements); + + @override + bool add(objc.ObjCObjectBase value) { + final alreadyContains = contains(value); + addObject_(value); + return !alreadyContains; + } + + @override + bool remove(Object? value) { + if (value is! objc.ObjCObjectBase) return false; + final alreadyContains = contains(value); + removeObject_(value); + return alreadyContains; + } + + @override + void clear() => removeAllObjects(); + NSMutableSet._(ffi.Pointer pointer, {bool retain = false, bool release = false}) : super.castFromPointer(pointer, retain: retain, release: release); @@ -9379,7 +9466,39 @@ interface class NSSecureCoding extends objc.ObjCProtocolBase /// NSSet class NSSet extends NSObject + with SetBase implements NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration { + /// Creates a [NSSet] from [elements]. + static NSSet of(Iterable elements) => + NSMutableSet.of(elements); + + @override + int get length => count; + + @override + bool contains(Object? element) => + element is objc.ObjCObjectBase ? containsObject_(element) : false; + + @override + objc.ObjCObjectBase? lookup(Object? element) => + element is objc.ObjCObjectBase ? member_(element) : null; + + @override + Iterator get iterator => objectEnumerator(); + + @override + Set toSet() => {...this}; + + @override + bool add(objc.ObjCObjectBase value) => + throw UnsupportedError("Cannot modify NSSet"); + + @override + bool remove(Object? value) => throw UnsupportedError("Cannot modify NSSet"); + + @override + void clear() => throw UnsupportedError("Cannot modify NSSet"); + NSSet._(ffi.Pointer pointer, {bool retain = false, bool release = false}) : super.castFromPointer(pointer, retain: retain, release: release); @@ -18498,3 +18617,43 @@ class _NSArrayIterator implements Iterator { return true; } } + +class _NSDictionaryKeyIterable with Iterable { + NSDictionary _dictionary; + + _NSDictionaryKeyIterable(this._dictionary); + + @override + int get length => _dictionary.length; + + @override + Iterator get iterator => + _NSDictionaryKeyIterator(_dictionary.keyEnumerator()); + + @override + bool contains(Object? key) => _dictionary.containsKey(key); +} + +class _NSDictionaryValueIterable with Iterable { + NSDictionary _dictionary; + + _NSDictionaryValueIterable(this._dictionary); + + @override + int get length => _dictionary.length; + + @override + Iterator get iterator => _dictionary.objectEnumerator(); +} + +class _NSDictionaryKeyIterator implements Iterator { + final Iterator _iterator; + _NSDictionaryKeyIterator(this._iterator); + + @override + NSCopying get current => NSCopying.castFrom(_iterator.current); + + @override + @pragma('vm:prefer-inline') + bool moveNext() => _iterator.moveNext(); +} diff --git a/pkgs/objective_c/test/nsdate_test.dart b/pkgs/objective_c/test/nsdate_test.dart new file mode 100644 index 0000000000..13fd3d982d --- /dev/null +++ b/pkgs/objective_c/test/nsdate_test.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2025, 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. + +// Objective C support is only available on mac. +@TestOn('mac-os') +library; + +import 'dart:ffi'; + +import 'package:objective_c/objective_c.dart'; +import 'package:test/test.dart'; + +void main() { + group('NSDate', () { + setUpAll(() { + // TODO(https://github.com/dart-lang/native/issues/1068): Remove this. + DynamicLibrary.open('test/objective_c.dylib'); + }); + + test('from DateTime', () { + final dartFirstAppeared = DateTime.utc(2011, 10, 10); + final nsDate = dartFirstAppeared.toNSDate(); + expect(nsDate.description.toDartString(), '2011-10-10 00:00:00 +0000'); + }); + + test('to DateTime', () { + final dartFirstAppeared = + NSDate.dateWithTimeIntervalSince1970_(1318204800); + final dateTime = dartFirstAppeared.toDateTime(); + expect(dateTime.toUtc().toString(), '2011-10-10 00:00:00.000Z'); + }); + }); +} diff --git a/pkgs/objective_c/test/nsdictionary_test.dart b/pkgs/objective_c/test/nsdictionary_test.dart new file mode 100644 index 0000000000..e92bf73441 --- /dev/null +++ b/pkgs/objective_c/test/nsdictionary_test.dart @@ -0,0 +1,113 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Objective C support is only available on mac. +@TestOn('mac-os') +library; + +import 'dart:ffi'; + +import 'package:objective_c/objective_c.dart'; +import 'package:test/test.dart'; + +void main() { + group('NSDictionary', () { + setUpAll(() { + // TODO(https://github.com/dart-lang/native/issues/1068): Remove this. + DynamicLibrary.open('test/objective_c.dylib'); + }); + + test('of', () { + final obj1 = 'obj1'.toNSString(); + final obj2 = 'obj2'.toNSString(); + final obj3 = 'obj3'.toNSString(); + final obj4 = 'obj4'.toNSString(); + final obj5 = 'obj5'.toNSString(); + final obj6 = 'obj6'.toNSString(); + + final dict = NSDictionary.of({ + obj1: obj2, + obj3: obj4, + obj5: obj6, + }); + + expect(dict.length, 3); + expect(dict[obj1], obj2); + expect(dict[obj3], obj4); + expect(dict[obj5], obj6); + + // Keys are copied, so compare the string values. + final actualKeys = []; + for (final key in dict.keys) { + actualKeys.add(NSString.castFrom(key).toDartString()); + } + expect(actualKeys, unorderedEquals(['obj1', 'obj3', 'obj5'])); + + // Values are stored by reference, so we can compare the actual objects. + final actualValues = []; + for (final value in dict.values) { + actualValues.add(value); + } + expect(actualValues, unorderedEquals([obj2, obj4, obj6])); + }); + + test('immutable', () { + final obj1 = 'obj1'.toNSString(); + final obj2 = 'obj2'.toNSString(); + final obj3 = 'obj3'.toNSString(); + final obj4 = 'obj4'.toNSString(); + final obj5 = 'obj5'.toNSString(); + final obj6 = 'obj6'.toNSString(); + + // NSDictionary.of actually returns a NSMutableDictionary, so our + // immutability tests wouldn't actually work. So convert it to a real + // NSDictionary using an ObjC constructor. + final dict = NSDictionary.dictionaryWithDictionary_(NSDictionary.of({ + obj1: obj2, + obj3: obj4, + obj5: obj6, + })); + + expect(() => dict[obj3] = obj1, throwsUnsupportedError); + expect(dict.clear, throwsUnsupportedError); + expect(() => dict.remove(obj1), throwsUnsupportedError); + }); + + test('MapBase mixin', () { + final obj1 = 'obj1'.toNSString(); + final obj2 = 'obj2'.toNSString(); + final obj3 = 'obj3'.toNSString(); + final obj4 = 'obj4'.toNSString(); + final obj5 = 'obj5'.toNSString(); + final obj6 = 'obj6'.toNSString(); + + final dict = NSDictionary.of({ + obj1: obj2, + obj3: obj4, + obj5: obj6, + }); + + expect(dict.isNotEmpty, isTrue); + expect(dict.containsKey(obj1), isTrue); + expect(dict.containsKey(obj4), isFalse); + expect(dict.containsValue(obj2), isTrue); + expect(dict.containsValue(obj3), isFalse); + + expect( + dict.map((key, value) => + MapEntry(value, key)), + { + obj2: obj1, + obj4: obj3, + obj6: obj5, + }); + expect( + dict.keys + .map((key) => NSString.castFrom(key).toDartString()) + .toList(), + unorderedEquals(['obj1', 'obj3', 'obj5'])); + expect(dict.values.toList(), unorderedEquals([obj2, obj4, obj6])); + }); + }); +} diff --git a/pkgs/objective_c/test/nsmutabledictionary_test.dart b/pkgs/objective_c/test/nsmutabledictionary_test.dart new file mode 100644 index 0000000000..11892f38cc --- /dev/null +++ b/pkgs/objective_c/test/nsmutabledictionary_test.dart @@ -0,0 +1,124 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Objective C support is only available on mac. +@TestOn('mac-os') +library; + +import 'dart:ffi'; + +import 'package:objective_c/objective_c.dart'; +import 'package:test/test.dart'; + +void main() { + group('NSMutableDictionary', () { + setUpAll(() { + // TODO(https://github.com/dart-lang/native/issues/1068): Remove this. + DynamicLibrary.open('test/objective_c.dylib'); + }); + + test('of', () { + final obj1 = 'obj1'.toNSString(); + final obj2 = 'obj2'.toNSString(); + final obj3 = 'obj3'.toNSString(); + final obj4 = 'obj4'.toNSString(); + final obj5 = 'obj5'.toNSString(); + final obj6 = 'obj6'.toNSString(); + + final dict = NSMutableDictionary.of({ + obj1: obj2, + obj3: obj4, + obj5: obj6, + }); + + expect(dict.length, 3); + expect(dict[obj1], obj2); + expect(dict[obj3], obj4); + expect(dict[obj5], obj6); + + // Keys are copied, so compare the string values. + final actualKeys = []; + for (final key in dict.keys) { + actualKeys.add(NSString.castFrom(key).toDartString()); + } + expect(actualKeys, unorderedEquals(['obj1', 'obj3', 'obj5'])); + + // Values are stored by reference, so we can compare the actual objects. + final actualValues = []; + for (final value in dict.values) { + actualValues.add(value); + } + expect(actualValues, unorderedEquals([obj2, obj4, obj6])); + }); + + test('mutable', () { + final obj1 = 'obj1'.toNSString(); + final obj2 = 'obj2'.toNSString(); + final obj3 = 'obj3'.toNSString(); + final obj4 = 'obj4'.toNSString(); + final obj5 = 'obj5'.toNSString(); + final obj6 = 'obj6'.toNSString(); + + final dict = NSMutableDictionary.of({ + obj1: obj2, + obj3: obj4, + obj5: obj6, + }); + + dict[obj3] = obj1; + expect(dict, { + obj1: obj2, + obj3: obj1, + obj5: obj6, + }); + + expect(dict.remove(null), null); + expect((dict as Map).remove(123), null); + expect(dict.remove(obj1), obj2); + expect(dict, { + obj3: obj1, + obj5: obj6, + }); + + dict.clear(); + expect(dict, {}); + }); + + test('MapBase mixin', () { + final obj1 = 'obj1'.toNSString(); + final obj2 = 'obj2'.toNSString(); + final obj3 = 'obj3'.toNSString(); + final obj4 = 'obj4'.toNSString(); + final obj5 = 'obj5'.toNSString(); + final obj6 = 'obj6'.toNSString(); + + final dict = NSMutableDictionary.of({ + obj1: obj2, + obj3: obj4, + obj5: obj6, + }); + + expect(dict.isNotEmpty, isTrue); + expect(dict.containsKey(obj1), isTrue); + expect(dict.containsKey(obj4), isFalse); + expect(dict.containsValue(obj2), isTrue); + expect(dict.containsValue(obj3), isFalse); + + expect( + dict.map((key, value) => + MapEntry(value, key)), + { + obj2: obj1, + obj4: obj3, + obj6: obj5, + }); + expect( + dict.keys + .map((key) => NSString.castFrom(key).toDartString()) + .toList(), + unorderedEquals(['obj1', 'obj3', 'obj5'])); + expect(dict.values.toList(), unorderedEquals([obj2, obj4, obj6])); + }); + }); +} diff --git a/pkgs/objective_c/test/nsmutableset_test.dart b/pkgs/objective_c/test/nsmutableset_test.dart new file mode 100644 index 0000000000..bf610b86a8 --- /dev/null +++ b/pkgs/objective_c/test/nsmutableset_test.dart @@ -0,0 +1,90 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Objective C support is only available on mac. +@TestOn('mac-os') +library; + +import 'dart:ffi'; + +import 'package:objective_c/objective_c.dart'; +import 'package:test/test.dart'; + +void main() { + group('NSMutableSet', () { + setUpAll(() { + // TODO(https://github.com/dart-lang/native/issues/1068): Remove this. + DynamicLibrary.open('test/objective_c.dylib'); + }); + + test('of', () { + final obj1 = NSObject(); + final obj2 = NSObject(); + final obj3 = NSObject(); + final obj4 = NSObject(); + final obj5 = NSObject(); + final expected = {obj1, obj2, obj3, obj4, obj5}; + final s = NSMutableSet.of(expected); + + expect(s.length, 5); + + expect(s.contains(obj3), isTrue); + expect(s.contains(NSObject()), isFalse); + expect((s as Set).contains(123), isFalse); + expect(s.contains(null), isFalse); + + expect(s.lookup(obj3), obj3); + expect(s.lookup(NSObject()), null); + expect((s as Set).lookup(123), null); + expect(s.lookup(null), null); + + final actual = []; + for (final value in s) { + actual.add(value); + } + expect(actual, expected); + + expect(s.toSet(), expected); + }); + + test('mutable', () { + final obj1 = NSObject(); + final obj2 = NSObject(); + final obj3 = NSObject(); + final obj4 = NSObject(); + final obj5 = NSObject(); + + final s = NSMutableSet.of({obj1, obj2, obj3, obj4, obj5}); + + final obj6 = NSObject(); + expect(s.add(obj1), isFalse); + expect(s.add(obj6), isTrue); + expect(s, {obj1, obj2, obj3, obj4, obj5, obj6}); + + final obj7 = NSObject(); + expect(s.remove(obj7), isFalse); + expect((s as Set).remove(123), isFalse); + expect(s.remove(null), isFalse); + expect(s.remove(obj3), isTrue); + expect(s, {obj1, obj2, obj4, obj5, obj6}); + + s.clear(); + expect(s, {}); + }); + + test('SetBase mixin', () { + final obj1 = NSObject(); + final obj2 = NSObject(); + final obj3 = NSObject(); + final obj4 = NSObject(); + final obj5 = NSObject(); + final expected = {obj1, obj2, obj3, obj4, obj5}; + final s = NSMutableSet.of(expected); + + expect(s.isNotEmpty, isTrue); + expect(s.intersection({obj5, obj2, null, 123}), {obj5, obj2}); + expect(s.toList(), expected); + }); + }); +} diff --git a/pkgs/objective_c/test/nsnumber_test.dart b/pkgs/objective_c/test/nsnumber_test.dart index 768d270fc9..ee945d3c37 100644 --- a/pkgs/objective_c/test/nsnumber_test.dart +++ b/pkgs/objective_c/test/nsnumber_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2025, 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. diff --git a/pkgs/objective_c/test/nsset_test.dart b/pkgs/objective_c/test/nsset_test.dart new file mode 100644 index 0000000000..b7eaa6a064 --- /dev/null +++ b/pkgs/objective_c/test/nsset_test.dart @@ -0,0 +1,82 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Objective C support is only available on mac. +@TestOn('mac-os') +library; + +import 'dart:ffi'; + +import 'package:objective_c/objective_c.dart'; +import 'package:test/test.dart'; + +void main() { + group('NSSet', () { + setUpAll(() { + // TODO(https://github.com/dart-lang/native/issues/1068): Remove this. + DynamicLibrary.open('test/objective_c.dylib'); + }); + + test('of', () { + final obj1 = NSObject(); + final obj2 = NSObject(); + final obj3 = NSObject(); + final obj4 = NSObject(); + final obj5 = NSObject(); + final expected = {obj1, obj2, obj3, obj4, obj5}; + final s = NSSet.of(expected); + + expect(s.length, 5); + + expect(s.contains(obj3), isTrue); + expect(s.contains(NSObject()), isFalse); + expect((s as Set).contains(123), isFalse); + expect(s.contains(null), isFalse); + + expect(s.lookup(obj3), obj3); + expect(s.lookup(NSObject()), null); + expect((s as Set).lookup(123), null); + expect(s.lookup(null), null); + + final actual = []; + for (final value in s) { + actual.add(value); + } + expect(actual, expected); + + expect(s.toSet(), expected); + }); + + test('immutable', () { + final obj1 = NSObject(); + final obj2 = NSObject(); + final obj3 = NSObject(); + final obj4 = NSObject(); + final obj5 = NSObject(); + + // NSSet.of actually returns a NSMutableSet, so our immutability tests + // wouldn't actually work. So convert it to a real NSSet using an ObjC + // constructor. + final s = NSSet.setWithSet_(NSSet.of({obj1, obj2, obj3, obj4, obj5})); + + expect(() => s.add(NSObject()), throwsUnsupportedError); + expect(() => s.remove(obj3), throwsUnsupportedError); + expect(s.clear, throwsUnsupportedError); + }); + + test('SetBase mixin', () { + final obj1 = NSObject(); + final obj2 = NSObject(); + final obj3 = NSObject(); + final obj4 = NSObject(); + final obj5 = NSObject(); + final expected = {obj1, obj2, obj3, obj4, obj5}; + final s = NSSet.of(expected); + + expect(s.isNotEmpty, isTrue); + expect(s.intersection({obj5, obj2, null, 123}), {obj5, obj2}); + expect(s.toList(), expected); + }); + }); +} diff --git a/pkgs/objective_c/tool/data/extra_methods.dart.in b/pkgs/objective_c/tool/data/extra_methods.dart.in index 2de1ebdc85..bf801bc770 100644 --- a/pkgs/objective_c/tool/data/extra_methods.dart.in +++ b/pkgs/objective_c/tool/data/extra_methods.dart.in @@ -2,7 +2,15 @@ // 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. -// Extra methods inserted into NSString by tool/generate_code.dart. +// Extra code inserted into lib/src/objective_c_bindings_generated.dart by +// tool/generate_code.dart. If a class with the same name is found in the +// generated code, the methods etc are merged. If there is no matching class, +// the class is added at the end of the generated code. + +// Note: tool/generate_code.dart uses simple regexes to parse the class +// declarations, so it's important that they remain unformatted, even if that +// means going over the 80 char width limit. The class bodies may be formatted. + class NSString { factory NSString(String str) { final cstr = str.toNativeUtf16(); @@ -12,7 +20,6 @@ class NSString { } } -// Extra methods inserted into NSArray by tool/generate_code.dart. class NSArray with Iterable { /// Creates a [NSArray] of the given length with [fill] at each position. /// @@ -36,8 +43,7 @@ class NSArray with Iterable { objc.ObjCObjectBase operator [](int index) => objectAtIndex_(index); } -// Extra methods inserted into NSMutableArray by tool/generate_code.dart. -class NSMutableArray with ListMixin { +class NSMutableArray with ListBase { /// Creates a [NSMutableArray] of the given length with [fill] at each /// position. /// @@ -59,17 +65,18 @@ class NSMutableArray with ListMixin { for (; len > newLength; --len) removeLastObject(); } + @override + Iterator get iterator => _NSArrayIterator(this); + + @override + objc.ObjCObjectBase operator [](int index) => objectAtIndex_(index); + @override void operator []=(int index, objc.ObjCObjectBase value) => replaceObjectAtIndex_withObject_(index, value); @override void add(objc.ObjCObjectBase value) => addObject_(value); - - @override - void addAll(Iterable iterable) { - for (final value in iterable) add(value); - } } class _NSArrayIterator implements Iterator { @@ -103,3 +110,170 @@ class _NSArrayIterator implements Iterator { } } +// Ideally we'd mixin UnmodifiableMapBase, but it's an ordinary class. So +// instead we mixin MapBase and then throw in all the modifying methods (which +// is essentially what UnmodifiableMapBase does anyway). +class NSDictionary with MapBase { + /// Creates a [NSDictionary] from [other]. + static NSDictionary of(Map other) => + NSMutableDictionary.of(other); + + @override + int get length => count; + + @override + objc.ObjCObjectBase? operator [](Object? key) => + key is NSCopying ? objectForKey_(key) : null; + + @override + Iterable get keys => _NSDictionaryKeyIterable(this); + + @override + Iterable get values => _NSDictionaryValueIterable(this); + + @override + bool containsKey(Object? key) => this[key] != null; + + @override + void operator []=(NSCopying key, objc.ObjCObjectBase value) => + throw UnsupportedError("Cannot modify NSDictionary"); + + @override + void clear() => throw UnsupportedError("Cannot modify NSDictionary"); + + @override + objc.ObjCObjectBase? remove(Object? key) => + throw UnsupportedError("Cannot modify NSDictionary"); +} + +class NSMutableDictionary { + /// Creates a [NSMutableDictionary] from [other]. + static NSDictionary of(Map other) => + NSMutableDictionary.dictionaryWithCapacity_(other.length)..addAll(other); + + @override + void clear() => removeAllObjects(); + + @override + objc.ObjCObjectBase? remove(Object? key) { + if (key is! NSCopying) return null; + final old = this[key]; + removeObjectForKey_(key); + return old; + } + + @override + void operator []=(NSCopying key, objc.ObjCObjectBase value) => + setObject_forKey_(value, NSCopying.castFrom(key)); +} + +class _NSDictionaryKeyIterable with Iterable { + NSDictionary _dictionary; + + _NSDictionaryKeyIterable(this._dictionary); + + @override + int get length => _dictionary.length; + + @override + Iterator get iterator => + _NSDictionaryKeyIterator(_dictionary.keyEnumerator()); + + @override + bool contains(Object? key) => _dictionary.containsKey(key); +} + +class _NSDictionaryValueIterable with Iterable { + NSDictionary _dictionary; + + _NSDictionaryValueIterable(this._dictionary); + + @override + int get length => _dictionary.length; + + @override + Iterator get iterator => _dictionary.objectEnumerator(); +} + +class NSEnumerator implements Iterator { + objc.ObjCObjectBase? _current; + + @override + objc.ObjCObjectBase get current => _current!; + + @override + @pragma('vm:prefer-inline') + bool moveNext() { + _current = nextObject(); + return _current != null; + } +} + +class _NSDictionaryKeyIterator implements Iterator { + final Iterator _iterator; + _NSDictionaryKeyIterator(this._iterator); + + @override + NSCopying get current => NSCopying.castFrom(_iterator.current); + + @override + @pragma('vm:prefer-inline') + bool moveNext() => _iterator.moveNext(); +} + +class NSSet with SetBase { + /// Creates a [NSSet] from [elements]. + static NSSet of(Iterable elements) => + NSMutableSet.of(elements); + + @override + int get length => count; + + @override + bool contains(Object? element) => + element is objc.ObjCObjectBase ? containsObject_(element) : false; + + @override + objc.ObjCObjectBase? lookup(Object? element) => + element is objc.ObjCObjectBase ? member_(element) : null; + + @override + Iterator get iterator => objectEnumerator(); + + @override + Set toSet() => { ...this }; + + @override + bool add(objc.ObjCObjectBase value) => + throw UnsupportedError("Cannot modify NSSet"); + + @override + bool remove(Object? value) => throw UnsupportedError("Cannot modify NSSet"); + + @override + void clear() => throw UnsupportedError("Cannot modify NSSet"); +} + +class NSMutableSet { + /// Creates a [NSMutableSet] from [elements]. + static NSMutableSet of(Iterable elements) => + setWithCapacity_(elements.length)..addAll(elements); + + @override + bool add(objc.ObjCObjectBase value) { + final alreadyContains = contains(value); + addObject_(value); + return !alreadyContains; + } + + @override + bool remove(Object? value) { + if (value is! objc.ObjCObjectBase) return false; + final alreadyContains = contains(value); + removeObject_(value); + return alreadyContains; + } + + @override + void clear() => removeAllObjects(); +} diff --git a/tool/ci.dart b/tool/ci.dart new file mode 100644 index 0000000000..72d29b7a88 --- /dev/null +++ b/tool/ci.dart @@ -0,0 +1,214 @@ +// Copyright (c) 2025, 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:args/args.dart'; +import 'package:yaml/yaml.dart'; + +void main(List arguments) async { + final parser = makeArgParser(); + + final ArgResults argResults = parser.parse(arguments); + + final packages = loadPackagesFromPubspec(); + + if (argResults['help'] as bool) { + print('A command-line tool for running CI checks on the CI or locally.'); + print(''); + print('Applies to the following packages:'); + for (final package in packages) { + print(' - $package'); + } + print(''); + print('Usage:'); + print(parser.usage); + return; + } + + if (argResults['pub'] as bool) { + const paths = [ + '.', + 'pkgs/hooks_runner/test_data/native_add_version_skew/', + 'pkgs/hooks_runner/test_data/native_add_version_skew_2/', + ]; + for (final path in paths) { + _runProcess('dart', ['pub', 'get', '--directory', path]); + } + } + + if (argResults['analyze'] as bool) { + _runProcess('dart', ['analyze', '--fatal-infos', ...packages]); + } + + if (argResults['format'] as bool) { + _runProcess('dart', [ + 'format', + '--output=none', + '--set-exit-if-changed', + ...packages, + ]); + } + + if (argResults['generate'] as bool) { + const generators = [ + 'pkgs/hooks/tool/generate_schemas.dart', + 'pkgs/hooks/tool/generate_syntax.dart', + 'pkgs/hooks/tool/normalize.dart', + ]; + for (final generator in generators) { + _runProcess('dart', [generator, '--set-exit-if-changed']); + } + } + + if (argResults['test'] as bool) { + final testUris = []; + for (final package in packages) { + final packageTestDirectory = Directory.fromUri( + repositoryRoot.resolve(package /*might end without slash*/), + ).uri.resolve('test/'); + if (Directory.fromUri(packageTestDirectory).existsSync()) { + final relativePath = packageTestDirectory.toFilePath().replaceAll( + repositoryRoot.toFilePath(), + '', + ); + testUris.add(relativePath); + } + } + _runProcess('dart', ['test', ...testUris]); + } + + if (argResults['example'] as bool) { + const examplesWithTest = [ + 'native_dynamic_linking', + 'native_add_app', + 'use_dart_api', + 'download_asset', + 'system_library', + ]; + for (final exampleWithTest in examplesWithTest) { + _runProcess( + workingDirectory: repositoryRoot.resolve( + 'pkgs/hooks/example/build/$exampleWithTest/', + ), + 'dart', + ['--enable-experiment=native-assets', 'test'], + ); + } + + _runProcess( + workingDirectory: repositoryRoot.resolve( + 'pkgs/hooks/example/build/native_add_app/', + ), + 'dart', + ['--enable-experiment=native-assets', 'run'], + ); + _runProcess( + workingDirectory: repositoryRoot.resolve( + 'pkgs/hooks/example/build/native_add_app/', + ), + 'dart', + ['--enable-experiment=native-assets', 'build', 'bin/native_add_app.dart'], + ); + _runProcess( + repositoryRoot + .resolve( + 'pkgs/hooks/example/build/native_add_app/bin/native_add_app/native_add_app.exe', + ) + .toFilePath(), + [], + ); + } +} + +ArgParser makeArgParser() { + final parser = + ArgParser() + ..addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Prints this help message.', + ) + ..addFlag( + 'analyze', + defaultsTo: true, + help: 'Run `dart analyze` on the packages.', + ) + ..addFlag( + 'example', + defaultsTo: true, + help: 'Run tests and executables for examples.', + ) + ..addFlag( + 'format', + defaultsTo: true, + help: 'Run `dart format` on the packages.', + ) + ..addFlag( + 'generate', + defaultsTo: true, + help: 'Run code generation scripts.', + ) + ..addFlag( + 'pub', + defaultsTo: false, + help: 'Run `dart pub get` on the root and non-workspace packages.', + ) + ..addFlag( + 'test', + defaultsTo: true, + help: 'Run `dart test` on the packages.', + ); + return parser; +} + +final Uri repositoryRoot = Platform.script.resolve('../'); + +/// Load the root packages from the workspace. Omit nested test/example packages. +List loadPackagesFromPubspec() { + final pubspecYaml = loadYaml( + File.fromUri(repositoryRoot.resolve('pubspec.yaml')).readAsStringSync(), + ); + final workspace = (pubspecYaml['workspace'] as List).cast(); + final packages = + workspace + .where( + (package) => + !package.contains('test_data') && !package.contains('example'), + ) + .toList(); + return packages; +} + +void _runProcess( + String executable, + List arguments, { + Uri? workingDirectory, +}) { + var commandString = '$executable ${arguments.join(' ')}'; + if (workingDirectory != null) { + commandString = 'cd ${workingDirectory.toFilePath()} && $commandString'; + } + print('+$commandString'); + final result = Process.runSync( + executable, + arguments, + workingDirectory: workingDirectory?.toFilePath(), + stderrEncoding: utf8, // Make ✓ from `dart test` show up on GitHub UI. + stdoutEncoding: utf8, + ); + + if (result.stdout.toString().isNotEmpty) { + print(result.stdout); + } + if (result.stderr.toString().isNotEmpty) { + print(result.stderr); + } + if (result.exitCode != 0) { + print('+$commandString failed with exitCode ${result.exitCode}.'); + exit(result.exitCode); + } +}