From c41903e9862980a72b2106f7250e5876bf5177d7 Mon Sep 17 00:00:00 2001 From: Mahesh Hegde <46179734+mahesh-hegde@users.noreply.github.com> Date: Mon, 14 Nov 2022 22:39:46 +0530 Subject: [PATCH] [jnigen] Improve coverage (#134) --- .github/workflows/test-package.yml | 69 +-- pkgs/jnigen/bin/jnigen.dart | 9 +- pkgs/jnigen/lib/src/bindings/common.dart | 2 +- pkgs/jnigen/lib/src/config/config.dart | 486 +---------------- .../lib/src/config/config_exception.dart | 12 + pkgs/jnigen/lib/src/config/config_types.dart | 494 ++++++++++++++++++ pkgs/jnigen/lib/src/config/yaml_reader.dart | 56 +- pkgs/jnigen/lib/src/elements/elements.dart | 39 +- pkgs/jnigen/lib/src/elements/elements.g.dart | 94 +--- pkgs/jnigen/lib/src/generate_bindings.dart | 15 +- pkgs/jnigen/lib/src/logging/logging.dart | 21 +- .../lib/src/tools/android_sdk_tools.dart | 59 ++- .../lib/src/tools/build_summarizer.dart | 7 + pkgs/jnigen/lib/src/tools/maven_tools.dart | 18 - pkgs/jnigen/lib/src/util/command_output.dart | 8 - pkgs/jnigen/lib/src/util/find_package.dart | 2 - pkgs/jnigen/pubspec.yaml | 4 +- pkgs/jnigen/test/config_test.dart | 45 +- .../jnigen/test/jackson_core_test/jnigen.yaml | 6 +- .../jnigen/test/regenerate_examples_test.dart | 56 ++ pkgs/jnigen/test/test_util/test_util.dart | 15 +- 21 files changed, 764 insertions(+), 753 deletions(-) create mode 100644 pkgs/jnigen/lib/src/config/config_exception.dart create mode 100644 pkgs/jnigen/lib/src/config/config_types.dart create mode 100644 pkgs/jnigen/test/regenerate_examples_test.dart diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 0129d5f60..765aae123 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -87,6 +87,12 @@ jobs: sudo apt-get install -y clang-format - name: Install dependencies run: dart pub get + - name: build in_app_java APK + run: flutter build apk --target-platform=android-arm64 + working-directory: pkgs/jnigen/example/in_app_java + - name: build notification_plugin example APK + run: flutter build apk --target-platform=android-arm64 + working-directory: pkgs/jnigen/example/notification_plugin/example - name: Run VM tests run: dart test --platform vm - name: Install coverage @@ -245,6 +251,12 @@ jobs: - run: Add-Content $env:GITHUB_PATH "$env:JAVA_HOME\bin\server" - run: dart pub get - run: dart run jnigen:setup + - name: build in_app_java APK + run: flutter build apk --target-platform=android-arm64 + working-directory: pkgs/jnigen/example/in_app_java + - name: build notification_plugin example APK + run: flutter build apk --target-platform=android-arm64 + working-directory: pkgs/jnigen/example/notification_plugin/example - run: dart test test_jni_macos_minimal: @@ -359,63 +371,6 @@ jobs: - run: flutter pub get - run: flutter build apk - build_notification_plugin_example: - runs-on: ubuntu-latest - defaults: - run: - working-directory: pkgs/jnigen/example/notification_plugin - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 - with: - distribution: 'zulu' - java-version: '11' - - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - cache: true - cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - - run: flutter pub get - - run: flutter analyze - - run: flutter build apk - working-directory: pkgs/jnigen/example/notification_plugin/example - - name: re-generate bindings - run: flutter pub run jnigen -Doutput.dart.path=_temp.dart -Doutput.c.path=_c/ --config jnigen.yaml - - name: compare generated dart bindings - run: diff lib/notifications.dart _temp.dart - - name: compare generated C bindings - run: diff -r src/ _c - - build_in_app_java_example: - runs-on: ubuntu-latest - defaults: - run: - working-directory: pkgs/jnigen/example/in_app_java - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 - with: - distribution: 'zulu' - java-version: '11' - - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - cache: true - cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - - name: install clang tools - run: | - sudo apt-get update -y - sudo apt-get install -y clang-format - - run: flutter pub get - - run: flutter analyze - - run: flutter build apk - - name: re-generate bindings - run: flutter pub run jnigen -Doutput.dart.path=_temp.dart -Doutput.c.path=_c/ --config jnigen.yaml - - name: compare generated dart bindings - run: diff lib/android_utils.dart _temp.dart - - name: compare generated C bindings - run: diff -r src/android_utils _c - run_pdfbox_example_linux: runs-on: ubuntu-latest defaults: diff --git a/pkgs/jnigen/bin/jnigen.dart b/pkgs/jnigen/bin/jnigen.dart index 01bae8560..a5065de57 100644 --- a/pkgs/jnigen/bin/jnigen.dart +++ b/pkgs/jnigen/bin/jnigen.dart @@ -3,8 +3,15 @@ // BSD-style license that can be found in the LICENSE file. import 'package:jnigen/jnigen.dart'; +import 'package:jnigen/src/logging/logging.dart'; void main(List args) async { - final config = Config.parseArgs(args); + Config config; + try { + config = Config.parseArgs(args); + } on ConfigException catch (e) { + log.fatal(e); + return; + } await generateJniBindings(config); } diff --git a/pkgs/jnigen/lib/src/bindings/common.dart b/pkgs/jnigen/lib/src/bindings/common.dart index ab1bc7f29..61931b8cf 100644 --- a/pkgs/jnigen/lib/src/bindings/common.dart +++ b/pkgs/jnigen/lib/src/bindings/common.dart @@ -207,7 +207,7 @@ abstract class BindingsGenerator { case Kind.typeVariable: case Kind.wildcard: throw SkipException( - 'Generic type parameters are not supported', t.toJson()); + 'Generic type parameters are not supported', t.name); case Kind.array: case Kind.declared: return voidPointer; diff --git a/pkgs/jnigen/lib/src/config/config.dart b/pkgs/jnigen/lib/src/config/config.dart index 56e09d008..c4168d86b 100644 --- a/pkgs/jnigen/lib/src/config/config.dart +++ b/pkgs/jnigen/lib/src/config/config.dart @@ -2,487 +2,5 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:io'; - -import 'package:jnigen/src/elements/elements.dart'; - -import 'yaml_reader.dart'; -import 'filters.dart'; - -import 'package:logging/logging.dart'; - -/// Configuration for dependencies to be downloaded using maven. -/// -/// Dependency names should be listed in groupId:artifactId:version format. -/// For [sourceDeps], sources will be unpacked to [sourceDir] root and JAR files -/// will also be downloaded. For the packages in jarOnlyDeps, only JAR files -/// will be downloaded. -/// -/// When passed as a parameter to [Config], the downloaded sources and -/// JAR files will be automatically added to source path and class path -/// respectively. -class MavenDownloads { - static const defaultMavenSourceDir = 'mvn_java'; - static const defaultMavenJarDir = 'mvn_jar'; - - MavenDownloads({ - this.sourceDeps = const [], - // ASK: Should this be changed to a gitignore'd directory like build ? - this.sourceDir = defaultMavenSourceDir, - this.jarOnlyDeps = const [], - this.jarDir = defaultMavenJarDir, - }); - List sourceDeps; - String sourceDir; - List jarOnlyDeps; - String jarDir; -} - -/// Configuration for Android SDK sources and stub JAR files. -/// -/// The SDK directories for platform stub JARs and sources are searched in the -/// same order in which [versions] are specified. -/// -/// If [includeSources] is true, `jnigen` searches for Android SDK sources -/// as well in the SDK directory and adds them to the source path. -/// -/// If [addGradleDeps] is true, a gradle stub is run in order to collect the -/// actual compile classpath of the `android/` subproject. -/// This will fail if there was no previous build of the project, or if a -/// `clean` task was run either through flutter or gradle wrapper. In such case, -/// it's required to run `flutter build apk` & retry running `jnigen`. -/// -/// A configuration is invalid if [versions] is unspecified or empty, and -/// [addGradleDeps] is also false. If [sdkRoot] is not specified but versions is -/// specified, an attempt is made to find out SDK installation directory using -/// environment variable `ANDROID_SDK_ROOT` if it's defined, else an error -/// will be thrown. -class AndroidSdkConfig { - AndroidSdkConfig({ - this.versions, - this.sdkRoot, - this.includeSources = false, - this.addGradleDeps = false, - this.androidExample, - }) { - if (versions != null && sdkRoot == null) { - throw ArgumentError("No SDK Root specified for finding Android SDK " - "from version priority list $versions"); - } - if (versions == null && !addGradleDeps) { - throw ArgumentError('Neither any SDK versions nor `addGradleDeps` ' - 'is specified. Unable to find Android libraries.'); - } - } - - /// Versions of android SDK to search for, in decreasing order of preference. - List? versions; - - /// Root of Android SDK installation, this should be normally given on - /// command line or by setting `ANDROID_SDK_ROOT`, since this varies from - /// system to system. - String? sdkRoot; - - /// Include downloaded android SDK sources in source path. - bool includeSources; - - /// Attempt to determine exact compile time dependencies by running a gradle - /// stub in android subproject of this project. - /// - /// An Android build must have happened before we are able to obtain classpath - /// of Gradle dependencies. Run `flutter build apk` before running a jnigen - /// script with this option. - /// - /// For the same reason, if the flutter project is a plugin instead of - /// application, it's not possible to determine the build classpath directly. - /// Please provide [androidExample] pointing to an example application in - /// that case. - bool addGradleDeps; - - /// Relative path to example application which will be used to determine - /// compile time classpath using a gradle stub. For most Android plugin - /// packages, 'example' will be the name of example application created inside - /// the package. This example should be built once before using this option, - /// so that gradle would have resolved all the dependencies. - String? androidExample; -} - -/// Additional options to pass to the summary generator component. -class SummarizerOptions { - SummarizerOptions( - {this.extraArgs = const [], this.workingDirectory, this.backend}); - List extraArgs; - Uri? workingDirectory; - String? backend; -} - -/// Backend for reading summary of Java libraries -enum SummarizerBackend { - /// Generate Java API summaries using JARs in provided `classPath`s. - asm, - - /// Generate Java API summaries using source files in provided `sourcePath`s. - doclet, -} - -T _getEnumValueFromString( - Map values, String? name, T defaultVal) { - if (name == null) return defaultVal; - final value = values[name]; - if (value == null) throw ArgumentError('Got: $name, allowed: ${values.keys}'); - return value; -} - -void _ensureIsDirectory(String name, Uri path) { - if (!path.toFilePath().endsWith(Platform.pathSeparator)) { - throw ArgumentError('$name must be a directory path. If using YAML ' - 'config, please ensure the path ends with a slash (/).'); - } -} - -enum OutputStructure { packageStructure, singleFile } - -OutputStructure getOutputStructure(String? name, OutputStructure defaultVal) { - const values = { - 'package_structure': OutputStructure.packageStructure, - 'single_file': OutputStructure.singleFile, - }; - return _getEnumValueFromString(values, name, defaultVal); -} - -enum BindingsType { cBased, dartOnly } - -BindingsType getBindingsType(String? name, BindingsType defaultVal) { - const values = { - 'c_based': BindingsType.cBased, - 'dart_only': BindingsType.dartOnly, - }; - return _getEnumValueFromString(values, name, defaultVal); -} - -class CCodeOutputConfig { - CCodeOutputConfig({ - required this.path, - required this.libraryName, - this.subdir, - }) { - _ensureIsDirectory('C output path', path); - if (subdir != null) { - _ensureIsDirectory('C subdirectory', path.resolve(subdir!)); - } - } - - /// Directory to write JNI C Bindings, in C+Dart mode. - /// - /// Strictly speaking, this is the root to place the `CMakeLists.txt` file - /// for the generated C bindings. It may be desirable to use the [subdir] - /// options to write C files to a subdirectory of [path]. For instance, - /// when generated code is required to be in `third_party` directory. - Uri path; - - /// Name of generated library in CMakeLists.txt configuration. - /// - /// This will also determine the name of shared object file. - String libraryName; - - /// Subfolder relative to [path] to write generated C code. - String? subdir; -} - -class DartCodeOutputConfig { - DartCodeOutputConfig({ - required this.path, - this.structure = OutputStructure.packageStructure, - }) { - if (structure == OutputStructure.singleFile) { - if (!path.toFilePath().endsWith('.dart')) { - throw ArgumentError( - 'output path must end with ".dart" in single file mode'); - } - } else { - _ensureIsDirectory('Dart output path', path); - } - } - - /// Path to write generated Dart bindings. - Uri path; - - /// File structure of the generated Dart bindings. - OutputStructure structure; -} - -class OutputConfig { - OutputConfig({ - this.bindingsType = BindingsType.cBased, - this.cConfig, - required this.dartConfig, - }) { - if (bindingsType == BindingsType.cBased && cConfig == null) { - throw ArgumentError("C output config must be provided!"); - } - } - BindingsType bindingsType; - DartCodeOutputConfig dartConfig; - CCodeOutputConfig? cConfig; -} - -class BindingExclusions { - BindingExclusions({this.methods, this.fields, this.classes}); - MethodFilter? methods; - FieldFilter? fields; - ClassFilter? classes; -} - -/// Configuration for jnigen binding generation. -class Config { - Config({ - required this.outputConfig, - required this.classes, - this.exclude, - this.sourcePath, - this.classPath, - this.preamble, - this.importMap, - this.androidSdkConfig, - this.mavenDownloads, - this.summarizerOptions, - this.logLevel = Level.INFO, - this.dumpJsonTo, - }); - - /// Output configuration for generated bindings - OutputConfig outputConfig; - - /// List of classes or packages for which bindings have to be generated. - /// - /// The names must be fully qualified, and it's assumed that the directory - /// structure corresponds to package naming. For example, com.abc.MyClass - /// should be resolvable as `com/abc/MyClass.java` from one of the provided - /// source paths. Same applies if ASM backend is used, except that the file - /// name suffix is `.class`. - List classes; - - /// Methods and fields to be excluded from generated bindings. - final BindingExclusions? exclude; - - /// Paths to search for java source files. - /// - /// If a source package is downloaded through [mavenDownloads] option, - /// the corresponding source folder is automatically added and does not - /// need to be explicitly specified. - List? sourcePath; - - /// class path for scanning java libraries. If [backend] is `asm`, the - /// specified classpath is used to search for [classes], otherwise it's - /// merely used by the doclet API to find transitively referenced classes, - /// but not the specified classes / packages themselves. - List? classPath; - - /// Common text to be pasted on top of generated C and Dart files. - final String? preamble; - - /// Additional java package -> dart package mappings (Experimental). - /// - /// a mapping com.abc.package -> 'package:my_package.dart/my_import.dart' - /// in this configuration suggests that any reference to a type from - /// com.abc.package shall resolve to an import of 'package:my_package.dart'. - /// - /// This can be as granular - /// `com.abc.package.Class -> 'package:abc/abc.dart'` - /// or coarse - /// `com.abc.package` -> 'package:abc/abc.dart'` - final Map? importMap; - - /// Configuration to search for Android SDK libraries (Experimental). - final AndroidSdkConfig? androidSdkConfig; - - /// Configuration for auto-downloading JAR / source packages using maven, - /// along with their transitive dependencies. - final MavenDownloads? mavenDownloads; - - /// Additional options for the summarizer component - final SummarizerOptions? summarizerOptions; - - /// Log verbosity. The possible values in decreasing order of verbosity - /// are verbose > debug > info > warning > error. Defaults to [LogLevel.info] - Level logLevel = Level.INFO; - - /// File to which JSON summary is written before binding generation. - final String? dumpJsonTo; - - static final _levels = Map.fromEntries( - Level.LEVELS.map((l) => MapEntry(l.name.toLowerCase(), l))); - static List? _toUris(List? paths) => - paths?.map(Uri.file).toList(); - - static Config parseArgs(List args) { - final prov = YamlReader.parseArgs(args); - final List missingValues = []; - T must(T? Function(String) f, T ifNull, String property) { - final res = f(property); - if (res == null) { - missingValues.add(property); - return ifNull; - } - return res; - } - - Uri? directoryUri(String? path) => - path != null ? Uri.directory(path) : null; - - MemberFilter? regexFilter(String property) { - final exclusions = prov.getStringList(property); - if (exclusions == null) return null; - final List> filters = []; - for (var exclusion in exclusions) { - final split = exclusion.split('#'); - if (split.length != 2) { - throw FormatException('Error parsing exclusion: "$exclusion": ' - 'expected to be in binaryName#member format.'); - } - filters.add(MemberNameFilter.exclude( - RegExp(split[0]), - RegExp(split[1]), - )); - } - return CombinedMemberFilter(filters); - } - - String? getSdkRoot() { - final root = prov.getString(_Props.androidSdkRoot) ?? - Platform.environment['ANDROID_SDK_ROOT']; - return root; - } - - Level logLevelFromString(String? levelName) { - if (levelName == null) return Level.INFO; - final level = _levels[levelName.toLowerCase()]; - if (level == null) { - throw ConfigError('Not a valid logging level: $levelName'); - } - return level; - } - - final config = Config( - sourcePath: _toUris(prov.getStringList(_Props.sourcePath)), - classPath: _toUris(prov.getStringList(_Props.classPath)), - classes: must(prov.getStringList, [], _Props.classes), - summarizerOptions: SummarizerOptions( - extraArgs: prov.getStringList(_Props.summarizerArgs) ?? const [], - backend: prov.getString(_Props.backend), - workingDirectory: - directoryUri(prov.getString(_Props.summarizerWorkingDir)), - ), - exclude: BindingExclusions( - methods: regexFilter(_Props.excludeMethods), - fields: regexFilter(_Props.excludeFields), - ), - outputConfig: OutputConfig( - bindingsType: getBindingsType( - prov.getString(_Props.bindingsType), - BindingsType.cBased, - ), - cConfig: prov.hasValue(_Props.cCodeOutputConfig) - ? CCodeOutputConfig( - libraryName: must(prov.getString, '', _Props.libraryName), - path: Uri.file(must(prov.getString, '.', _Props.cRoot)), - subdir: prov.getString(_Props.cSubdir), - ) - : null, - dartConfig: DartCodeOutputConfig( - path: Uri.file(must(prov.getString, '.', _Props.dartRoot)), - structure: getOutputStructure( - prov.getString(_Props.outputStructure), - OutputStructure.packageStructure, - ), - ), - ), - preamble: prov.getString(_Props.preamble), - importMap: prov.getStringMap(_Props.importMap), - mavenDownloads: prov.hasValue(_Props.mavenDownloads) - ? MavenDownloads( - sourceDeps: prov.getStringList(_Props.sourceDeps) ?? const [], - sourceDir: prov.getString(_Props.mavenSourceDir) ?? - MavenDownloads.defaultMavenSourceDir, - jarOnlyDeps: prov.getStringList(_Props.jarOnlyDeps) ?? const [], - jarDir: prov.getString(_Props.mavenJarDir) ?? - MavenDownloads.defaultMavenJarDir, - ) - : null, - androidSdkConfig: prov.hasValue(_Props.androidSdkConfig) - ? AndroidSdkConfig( - versions: prov - .getStringList(_Props.androidSdkVersions) - ?.map(int.parse) - .toList(), - sdkRoot: getSdkRoot(), - includeSources: - prov.getBool(_Props.includeAndroidSources) ?? false, - addGradleDeps: prov.getBool(_Props.addGradleDeps) ?? false, - androidExample: prov.getString(_Props.androidExample), - ) - : null, - logLevel: logLevelFromString( - prov.getOneOf( - _Props.logLevel, - _levels.keys.toSet(), - ), - ), - ); - if (missingValues.isNotEmpty) { - stderr.write('Following config values are required but not provided\n' - 'Please provide these properties through YAML ' - 'or use the command line switch -D=.\n'); - for (var missing in missingValues) { - stderr.writeln('* $missing'); - } - if (missingValues.contains(_Props.androidSdkRoot)) { - stderr.writeln('Please specify ${_Props.androidSdkRoot} through ' - 'command line or ensure that the ANDROID_SDK_ROOT environment ' - 'variable is set.'); - } - exit(1); - } - return config; - } -} - -class _Props { - static const summarizer = 'summarizer'; - static const summarizerArgs = '$summarizer.extra_args'; - static const summarizerWorkingDir = '$summarizer.working_dir'; - static const backend = '$summarizer.backend'; - - static const sourcePath = 'source_path'; - static const classPath = 'class_path'; - static const classes = 'classes'; - static const exclude = 'exclude'; - static const excludeMethods = '$exclude.methods'; - static const excludeFields = '$exclude.fields'; - - static const importMap = 'import_map'; - static const outputConfig = 'output'; - static const bindingsType = '$outputConfig.bindings_type'; - static const cCodeOutputConfig = '$outputConfig.c'; - static const dartCodeOutputConfig = '$outputConfig.dart'; - static const cRoot = '$cCodeOutputConfig.path'; - static const cSubdir = '$cCodeOutputConfig.subdir'; - static const dartRoot = '$dartCodeOutputConfig.path'; - static const outputStructure = '$dartCodeOutputConfig.structure'; - static const libraryName = '$cCodeOutputConfig.library_name'; - static const preamble = 'preamble'; - static const logLevel = 'log_level'; - - static const mavenDownloads = 'maven_downloads'; - static const sourceDeps = '$mavenDownloads.source_deps'; - static const mavenSourceDir = '$mavenDownloads.source_dir'; - static const jarOnlyDeps = '$mavenDownloads.jar_only_deps'; - static const mavenJarDir = '$mavenDownloads.jar_dir'; - - static const androidSdkConfig = 'android_sdk_config'; - static const androidSdkRoot = '$androidSdkConfig.sdk_root'; - static const androidSdkVersions = '$androidSdkConfig.versions'; - static const includeAndroidSources = '$androidSdkConfig.include_sources'; - static const addGradleDeps = '$androidSdkConfig.add_gradle_deps'; - static const androidExample = '$androidSdkConfig.android_example'; -} +export 'config_types.dart'; +export 'config_exception.dart'; diff --git a/pkgs/jnigen/lib/src/config/config_exception.dart b/pkgs/jnigen/lib/src/config/config_exception.dart new file mode 100644 index 000000000..d3518cc15 --- /dev/null +++ b/pkgs/jnigen/lib/src/config/config_exception.dart @@ -0,0 +1,12 @@ +// 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. + +/// Exception thrown when a configuration value is invalid. +class ConfigException implements Exception { + ConfigException(this.message); + String message; + + @override + String toString() => 'Error parsing configuration: $message'; +} diff --git a/pkgs/jnigen/lib/src/config/config_types.dart b/pkgs/jnigen/lib/src/config/config_types.dart new file mode 100644 index 000000000..c32f82479 --- /dev/null +++ b/pkgs/jnigen/lib/src/config/config_types.dart @@ -0,0 +1,494 @@ +// 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. + +import 'dart:io'; + +import 'package:jnigen/src/elements/elements.dart'; + +import 'yaml_reader.dart'; +import 'filters.dart'; +import 'config_exception.dart'; + +import 'package:logging/logging.dart'; + +/// Configuration for dependencies to be downloaded using maven. +/// +/// Dependency names should be listed in groupId:artifactId:version format. +/// For [sourceDeps], sources will be unpacked to [sourceDir] root and JAR files +/// will also be downloaded. For the packages in jarOnlyDeps, only JAR files +/// will be downloaded. +/// +/// When passed as a parameter to [Config], the downloaded sources and +/// JAR files will be automatically added to source path and class path +/// respectively. +class MavenDownloads { + static const defaultMavenSourceDir = 'mvn_java'; + static const defaultMavenJarDir = 'mvn_jar'; + + MavenDownloads({ + this.sourceDeps = const [], + // ASK: Should this be changed to a gitignore'd directory like build ? + this.sourceDir = defaultMavenSourceDir, + this.jarOnlyDeps = const [], + this.jarDir = defaultMavenJarDir, + }); + List sourceDeps; + String sourceDir; + List jarOnlyDeps; + String jarDir; +} + +/// Configuration for Android SDK sources and stub JAR files. +/// +/// The SDK directories for platform stub JARs and sources are searched in the +/// same order in which [versions] are specified. +/// +/// If [addGradleDeps] is true, a gradle stub is run in order to collect the +/// actual compile classpath of the `android/` subproject. +/// This will fail if there was no previous build of the project, or if a +/// `clean` task was run either through flutter or gradle wrapper. In such case, +/// it's required to run `flutter build apk` & retry running `jnigen`. +/// +/// A configuration is invalid if [versions] is unspecified or empty, and +/// [addGradleDeps] is also false. If [sdkRoot] is not specified but versions is +/// specified, an attempt is made to find out SDK installation directory using +/// environment variable `ANDROID_SDK_ROOT` if it's defined, else an error +/// will be thrown. +class AndroidSdkConfig { + AndroidSdkConfig({ + this.versions, + this.sdkRoot, + this.addGradleDeps = false, + this.androidExample, + }) { + if (versions != null && sdkRoot == null) { + throw ConfigException("No SDK Root specified for finding Android SDK " + "from version priority list $versions"); + } + if (versions == null && !addGradleDeps) { + throw ConfigException('Neither any SDK versions nor `addGradleDeps` ' + 'is specified. Unable to find Android libraries.'); + } + } + + /// Versions of android SDK to search for, in decreasing order of preference. + List? versions; + + /// Root of Android SDK installation, this should be normally given on + /// command line or by setting `ANDROID_SDK_ROOT`, since this varies from + /// system to system. + String? sdkRoot; + + /// Attempt to determine exact compile time dependencies by running a gradle + /// stub in android subproject of this project. + /// + /// An Android build must have happened before we are able to obtain classpath + /// of Gradle dependencies. Run `flutter build apk` before running a jnigen + /// script with this option. + /// + /// For the same reason, if the flutter project is a plugin instead of + /// application, it's not possible to determine the build classpath directly. + /// Please provide [androidExample] pointing to an example application in + /// that case. + bool addGradleDeps; + + /// Relative path to example application which will be used to determine + /// compile time classpath using a gradle stub. For most Android plugin + /// packages, 'example' will be the name of example application created inside + /// the package. This example should be built once before using this option, + /// so that gradle would have resolved all the dependencies. + String? androidExample; +} + +/// Additional options to pass to the summary generator component. +class SummarizerOptions { + SummarizerOptions( + {this.extraArgs = const [], this.workingDirectory, this.backend}); + List extraArgs; + Uri? workingDirectory; + String? backend; +} + +/// Backend for reading summary of Java libraries +enum SummarizerBackend { + /// Generate Java API summaries using JARs in provided `classPath`s. + asm, + + /// Generate Java API summaries using source files in provided `sourcePath`s. + doclet, +} + +T _getEnumValueFromString( + Map values, String? name, T defaultVal) { + if (name == null) return defaultVal; + final value = values[name]; + if (value == null) { + throw ConfigException('Got: $name, allowed: ${values.keys}'); + } + return value; +} + +void _ensureIsDirectory(String name, Uri path) { + if (!path.toFilePath().endsWith(Platform.pathSeparator)) { + throw ConfigException('$name must be a directory path. If using YAML ' + 'config, please ensure the path ends with a slash (/).'); + } +} + +enum OutputStructure { packageStructure, singleFile } + +OutputStructure getOutputStructure(String? name, OutputStructure defaultVal) { + const values = { + 'package_structure': OutputStructure.packageStructure, + 'single_file': OutputStructure.singleFile, + }; + return _getEnumValueFromString(values, name, defaultVal); +} + +enum BindingsType { cBased, dartOnly } + +BindingsType getBindingsType(String? name, BindingsType defaultVal) { + const values = { + 'c_based': BindingsType.cBased, + 'dart_only': BindingsType.dartOnly, + }; + return _getEnumValueFromString(values, name, defaultVal); +} + +class CCodeOutputConfig { + CCodeOutputConfig({ + required this.path, + required this.libraryName, + this.subdir, + }) { + _ensureIsDirectory('C output path', path); + if (subdir != null) { + _ensureIsDirectory('C subdirectory', path.resolve(subdir!)); + } + } + + /// Directory to write JNI C Bindings, in C+Dart mode. + /// + /// Strictly speaking, this is the root to place the `CMakeLists.txt` file + /// for the generated C bindings. It may be desirable to use the [subdir] + /// options to write C files to a subdirectory of [path]. For instance, + /// when generated code is required to be in `third_party` directory. + Uri path; + + /// Name of generated library in CMakeLists.txt configuration. + /// + /// This will also determine the name of shared object file. + String libraryName; + + /// Subfolder relative to [path] to write generated C code. + String? subdir; +} + +class DartCodeOutputConfig { + DartCodeOutputConfig({ + required this.path, + this.structure = OutputStructure.packageStructure, + }) { + if (structure == OutputStructure.singleFile) { + if (!path.toFilePath().endsWith('.dart')) { + throw ConfigException( + 'output path must end with ".dart" in single file mode'); + } + } else { + _ensureIsDirectory('Dart output path', path); + } + } + + /// Path to write generated Dart bindings. + Uri path; + + /// File structure of the generated Dart bindings. + OutputStructure structure; +} + +class OutputConfig { + OutputConfig({ + this.bindingsType = BindingsType.cBased, + this.cConfig, + required this.dartConfig, + }) { + if (bindingsType == BindingsType.cBased && cConfig == null) { + throw ConfigException("C output config must be provided!"); + } + } + BindingsType bindingsType; + DartCodeOutputConfig dartConfig; + CCodeOutputConfig? cConfig; +} + +class BindingExclusions { + BindingExclusions({this.methods, this.fields, this.classes}); + MethodFilter? methods; + FieldFilter? fields; + ClassFilter? classes; +} + +/// Configuration for jnigen binding generation. +class Config { + Config({ + required this.outputConfig, + required this.classes, + this.exclude, + this.sourcePath, + this.classPath, + this.preamble, + this.importMap, + this.androidSdkConfig, + this.mavenDownloads, + this.summarizerOptions, + this.logLevel = Level.INFO, + this.dumpJsonTo, + }); + + /// Output configuration for generated bindings + OutputConfig outputConfig; + + /// List of classes or packages for which bindings have to be generated. + /// + /// The names must be fully qualified, and it's assumed that the directory + /// structure corresponds to package naming. For example, com.abc.MyClass + /// should be resolvable as `com/abc/MyClass.java` from one of the provided + /// source paths. Same applies if ASM backend is used, except that the file + /// name suffix is `.class`. + List classes; + + /// Methods and fields to be excluded from generated bindings. + final BindingExclusions? exclude; + + /// Paths to search for java source files. + /// + /// If a source package is downloaded through [mavenDownloads] option, + /// the corresponding source folder is automatically added and does not + /// need to be explicitly specified. + List? sourcePath; + + /// class path for scanning java libraries. If [backend] is `asm`, the + /// specified classpath is used to search for [classes], otherwise it's + /// merely used by the doclet API to find transitively referenced classes, + /// but not the specified classes / packages themselves. + List? classPath; + + /// Common text to be pasted on top of generated C and Dart files. + final String? preamble; + + /// Additional java package -> dart package mappings (Experimental). + /// + /// a mapping com.abc.package -> 'package:my_package.dart/my_import.dart' + /// in this configuration suggests that any reference to a type from + /// com.abc.package shall resolve to an import of 'package:my_package.dart'. + /// + /// This can be as granular + /// `com.abc.package.Class -> 'package:abc/abc.dart'` + /// or coarse + /// `com.abc.package` -> 'package:abc/abc.dart'` + final Map? importMap; + + /// Configuration to search for Android SDK libraries (Experimental). + final AndroidSdkConfig? androidSdkConfig; + + /// Configuration for auto-downloading JAR / source packages using maven, + /// along with their transitive dependencies. + final MavenDownloads? mavenDownloads; + + /// Additional options for the summarizer component + final SummarizerOptions? summarizerOptions; + + /// Directory containing the YAML configuration file, if any. + Uri? get configRoot => _configRoot; + Uri? _configRoot; + + /// Log verbosity. The possible values in decreasing order of verbosity + /// are verbose > debug > info > warning > error. Defaults to [LogLevel.info] + Level logLevel = Level.INFO; + + /// File to which JSON summary is written before binding generation. + final String? dumpJsonTo; + + static final _levels = Map.fromEntries( + Level.LEVELS.map((l) => MapEntry(l.name.toLowerCase(), l))); + static List? _toUris(List? paths) => + paths?.map(Uri.file).toList(); + + static Config parseArgs(List args) { + final prov = YamlReader.parseArgs(args); + + final List missingValues = []; + + T must(T? Function(String) f, T ifNull, String property) { + final res = f(property); + if (res == null) { + missingValues.add(property); + return ifNull; + } + return res; + } + + Uri? directoryUri(String? path) => + path != null ? Uri.directory(path) : null; + + MemberFilter? regexFilter(String property) { + final exclusions = prov.getStringList(property); + if (exclusions == null) return null; + final List> filters = []; + for (var exclusion in exclusions) { + final split = exclusion.split('#'); + if (split.length != 2) { + throw ConfigException('Error parsing exclusion: "$exclusion": ' + 'expected to be in binaryName#member format.'); + } + filters.add(MemberNameFilter.exclude( + RegExp(split[0]), + RegExp(split[1]), + )); + } + return CombinedMemberFilter(filters); + } + + String? getSdkRoot() { + final root = prov.getString(_Props.androidSdkRoot) ?? + Platform.environment['ANDROID_SDK_ROOT']; + return root; + } + + Level logLevelFromString(String? levelName) { + if (levelName == null) return Level.INFO; + final level = _levels[levelName.toLowerCase()]; + if (level == null) { + throw ConfigException('Not a valid logging level: $levelName'); + } + return level; + } + + final configRoot = prov.getConfigRoot(); + String resolveFromConfigRoot(String reference) => + configRoot?.resolve(reference).toFilePath() ?? reference; + + final config = Config( + sourcePath: _toUris(prov.getPathList(_Props.sourcePath)), + classPath: _toUris(prov.getPathList(_Props.classPath)), + classes: must(prov.getStringList, [], _Props.classes), + summarizerOptions: SummarizerOptions( + extraArgs: prov.getStringList(_Props.summarizerArgs) ?? const [], + backend: prov.getString(_Props.backend), + workingDirectory: + directoryUri(prov.getPath(_Props.summarizerWorkingDir)), + ), + exclude: BindingExclusions( + methods: regexFilter(_Props.excludeMethods), + fields: regexFilter(_Props.excludeFields), + ), + outputConfig: OutputConfig( + bindingsType: getBindingsType( + prov.getString(_Props.bindingsType), + BindingsType.cBased, + ), + cConfig: prov.hasValue(_Props.cCodeOutputConfig) + ? CCodeOutputConfig( + libraryName: must(prov.getString, '', _Props.libraryName), + path: Uri.file(must(prov.getPath, '.', _Props.cRoot)), + subdir: prov.getString(_Props.cSubdir), + ) + : null, + dartConfig: DartCodeOutputConfig( + path: Uri.file(must(prov.getPath, '.', _Props.dartRoot)), + structure: getOutputStructure( + prov.getString(_Props.outputStructure), + OutputStructure.packageStructure, + ), + ), + ), + preamble: prov.getString(_Props.preamble), + importMap: prov.getStringMap(_Props.importMap), + mavenDownloads: prov.hasValue(_Props.mavenDownloads) + ? MavenDownloads( + sourceDeps: prov.getStringList(_Props.sourceDeps) ?? const [], + sourceDir: prov.getPath(_Props.mavenSourceDir) ?? + resolveFromConfigRoot(MavenDownloads.defaultMavenSourceDir), + jarOnlyDeps: prov.getStringList(_Props.jarOnlyDeps) ?? const [], + jarDir: prov.getPath(_Props.mavenJarDir) ?? + resolveFromConfigRoot(MavenDownloads.defaultMavenJarDir), + ) + : null, + androidSdkConfig: prov.hasValue(_Props.androidSdkConfig) + ? AndroidSdkConfig( + versions: prov + .getStringList(_Props.androidSdkVersions) + ?.map(int.parse) + .toList(), + sdkRoot: getSdkRoot(), + addGradleDeps: prov.getBool(_Props.addGradleDeps) ?? false, + // Leaving this as getString instead of getPath, because + // it's resolved later in android_sdk_tools. + androidExample: prov.getString(_Props.androidExample), + ) + : null, + logLevel: logLevelFromString( + prov.getOneOf( + _Props.logLevel, + _levels.keys.toSet(), + ), + ), + ); + if (missingValues.isNotEmpty) { + stderr.write('Following config values are required but not provided\n' + 'Please provide these properties through YAML ' + 'or use the command line switch -D=.\n'); + for (var missing in missingValues) { + stderr.writeln('* $missing'); + } + if (missingValues.contains(_Props.androidSdkRoot)) { + stderr.writeln('Please specify ${_Props.androidSdkRoot} through ' + 'command line or ensure that the ANDROID_SDK_ROOT environment ' + 'variable is set.'); + } + exit(1); + } + config._configRoot = configRoot; + return config; + } +} + +class _Props { + static const summarizer = 'summarizer'; + static const summarizerArgs = '$summarizer.extra_args'; + static const summarizerWorkingDir = '$summarizer.working_dir'; + static const backend = '$summarizer.backend'; + + static const sourcePath = 'source_path'; + static const classPath = 'class_path'; + static const classes = 'classes'; + static const exclude = 'exclude'; + static const excludeMethods = '$exclude.methods'; + static const excludeFields = '$exclude.fields'; + + static const importMap = 'import_map'; + static const outputConfig = 'output'; + static const bindingsType = '$outputConfig.bindings_type'; + static const cCodeOutputConfig = '$outputConfig.c'; + static const dartCodeOutputConfig = '$outputConfig.dart'; + static const cRoot = '$cCodeOutputConfig.path'; + static const cSubdir = '$cCodeOutputConfig.subdir'; + static const dartRoot = '$dartCodeOutputConfig.path'; + static const outputStructure = '$dartCodeOutputConfig.structure'; + static const libraryName = '$cCodeOutputConfig.library_name'; + static const preamble = 'preamble'; + static const logLevel = 'log_level'; + + static const mavenDownloads = 'maven_downloads'; + static const sourceDeps = '$mavenDownloads.source_deps'; + static const mavenSourceDir = '$mavenDownloads.source_dir'; + static const jarOnlyDeps = '$mavenDownloads.jar_only_deps'; + static const mavenJarDir = '$mavenDownloads.jar_dir'; + + static const androidSdkConfig = 'android_sdk_config'; + static const androidSdkRoot = '$androidSdkConfig.sdk_root'; + static const androidSdkVersions = '$androidSdkConfig.versions'; + static const addGradleDeps = '$androidSdkConfig.add_gradle_deps'; + static const androidExample = '$androidSdkConfig.android_example'; +} diff --git a/pkgs/jnigen/lib/src/config/yaml_reader.dart b/pkgs/jnigen/lib/src/config/yaml_reader.dart index 8f3aa6701..4347ecb24 100644 --- a/pkgs/jnigen/lib/src/config/yaml_reader.dart +++ b/pkgs/jnigen/lib/src/config/yaml_reader.dart @@ -7,20 +7,14 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:yaml/yaml.dart'; -class ConfigError extends Error { - ConfigError(this.message); - final String message; - @override - String toString() => message; -} +import 'config_exception.dart'; /// YAML Reader which enables to override specific values from command line. class YamlReader { - YamlReader.of(this.cli, this.yaml); - YamlReader.fromYaml(this.yaml) : cli = const {}; - YamlReader.fromMap(this.cli) : yaml = const {}; + YamlReader.of(this.cli, this.yaml, this.yamlFile); Map cli; Map yaml; + File? yamlFile; /// Parses the provided command line arguments and returns a [YamlReader]. /// @@ -57,7 +51,7 @@ class YamlReader { if (yamlInput is Map) { yamlMap = yamlInput; } else { - throw ConfigError('YAML config must be set of key value pairs'); + throw ConfigException('YAML config must be set of key value pairs'); } } on Exception catch (e) { stderr.writeln('cannot read $configFile: $e'); @@ -72,10 +66,11 @@ class YamlReader { final propertyValue = match.group(2); properties[propertyName!] = propertyValue!; } else { - throw ConfigError('override does not match expected pattern'); + throw ConfigException('override does not match expected pattern'); } } - return YamlReader.of(properties, yamlMap); + return YamlReader.of( + properties, yamlMap, configFile != null ? File(configFile) : null); } bool? getBool(String property) { @@ -87,7 +82,7 @@ class YamlReader { if (v == 'false') { return false; } - throw ConfigError('expected boolean value for $property, got $v'); + throw ConfigException('expected boolean value for $property, got $v'); } return getYamlValue(property); } @@ -97,18 +92,43 @@ class YamlReader { return configValue; } + /// Same as [getString] but path is resolved relative to YAML config if it's + /// from YAML config. + String? getPath(String property) { + final cliOverride = cli[property]; + if (cliOverride != null) return cliOverride; + final path = getYamlValue(property); + if (path == null) return null; + // In (very unlikely) case YAML config didn't come from a file, + // do not try to resolve anything. + if (yamlFile == null) return path; + final yamlDir = yamlFile!.parent; + return yamlDir.uri.resolve(path).toFilePath(); + } + List? getStringList(String property) { final configValue = cli[property]?.split(';') ?? getYamlValue(property)?.cast(); return configValue; } + List? getPathList(String property) { + final cliOverride = cli[property]?.split(';'); + if (cliOverride != null) return cliOverride; + final paths = getYamlValue(property)?.cast(); + if (paths == null) return null; + // In (very unlikely) case YAML config didn't come from a file. + if (yamlFile == null) return paths; + final yamlDir = yamlFile!.parent; + return paths.map((path) => yamlDir.uri.resolve(path).toFilePath()).toList(); + } + String? getOneOf(String property, Set values) { final value = cli[property] ?? getYamlValue(property); if (value == null || values.contains(value)) { return value; } - throw ConfigError('expected one of $values for $property'); + throw ConfigException('expected one of $values for $property'); } Map? getStringMap(String property) { @@ -126,7 +146,7 @@ class YamlReader { if (cursor is YamlMap || cursor is Map) { cursor = cursor[i]; } else { - throw ConfigError('expected $current to be a YAML map'); + throw ConfigException('expected $current to be a YAML map'); } current = [if (current != '') current, i].join('.'); if (cursor == null) { @@ -134,8 +154,12 @@ class YamlReader { } } if (cursor is! T) { - throw ConfigError('expected $T for $property, got ${cursor.runtimeType}'); + throw ConfigException( + 'expected $T for $property, got ${cursor.runtimeType}'); } return cursor; } + + /// Returns URI of the directory containing YAML config. + Uri? getConfigRoot() => yamlFile?.parent.uri; } diff --git a/pkgs/jnigen/lib/src/elements/elements.dart b/pkgs/jnigen/lib/src/elements/elements.dart index 1d0581efe..0c07b4323 100644 --- a/pkgs/jnigen/lib/src/elements/elements.dart +++ b/pkgs/jnigen/lib/src/elements/elements.dart @@ -24,7 +24,7 @@ enum DeclKind { // JSON. this allows us to reduce JSON size by providing Include.NON_NULL // option in java. -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class ClassDecl { /// Methods & properties already defined by dart JObject base class. static const Map _definedSyms = { @@ -90,7 +90,6 @@ class ClassDecl { factory ClassDecl.fromJson(Map json) => _$ClassDeclFromJson(json); - Map toJson() => _$ClassDeclToJson(this); String get internalName => binaryName.replaceAll(".", "/"); @@ -141,7 +140,7 @@ enum Kind { array, } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class TypeUsage { TypeUsage({ required this.shorthand, @@ -182,14 +181,13 @@ class TypeUsage { } return t; } - Map toJson() => _$TypeUsageToJson(this); } abstract class ReferredType { String get name; } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class PrimitiveType implements ReferredType { PrimitiveType({required this.name}); @@ -198,10 +196,9 @@ class PrimitiveType implements ReferredType { factory PrimitiveType.fromJson(Map json) => _$PrimitiveTypeFromJson(json); - Map toJson() => _$PrimitiveTypeToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class DeclaredType implements ReferredType { DeclaredType({ required this.binaryName, @@ -216,10 +213,9 @@ class DeclaredType implements ReferredType { factory DeclaredType.fromJson(Map json) => _$DeclaredTypeFromJson(json); - Map toJson() => _$DeclaredTypeToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class TypeVar implements ReferredType { TypeVar({required this.name}); @override @@ -227,10 +223,9 @@ class TypeVar implements ReferredType { factory TypeVar.fromJson(Map json) => _$TypeVarFromJson(json); - Map toJson() => _$TypeVarToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class Wildcard implements ReferredType { Wildcard({this.extendsBound, this.superBound}); TypeUsage? extendsBound, superBound; @@ -240,10 +235,9 @@ class Wildcard implements ReferredType { factory Wildcard.fromJson(Map json) => _$WildcardFromJson(json); - Map toJson() => _$WildcardToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class ArrayType implements ReferredType { ArrayType({required this.type}); TypeUsage type; @@ -253,14 +247,13 @@ class ArrayType implements ReferredType { factory ArrayType.fromJson(Map json) => _$ArrayTypeFromJson(json); - Map toJson() => _$ArrayTypeToJson(this); } abstract class ClassMember { String get name; } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class Method implements ClassMember { Method( {this.annotations = const [], @@ -297,10 +290,9 @@ class Method implements ClassMember { } factory Method.fromJson(Map json) => _$MethodFromJson(json); - Map toJson() => _$MethodToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class Param { Param( {this.annotations = const [], @@ -314,10 +306,9 @@ class Param { TypeUsage type; factory Param.fromJson(Map json) => _$ParamFromJson(json); - Map toJson() => _$ParamToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class Field implements ClassMember { Field( {this.annotations = const [], @@ -344,10 +335,9 @@ class Field implements ClassMember { bool isIncluded = true; factory Field.fromJson(Map json) => _$FieldFromJson(json); - Map toJson() => _$FieldToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class TypeParam { TypeParam({required this.name, this.bounds = const []}); String name; @@ -358,10 +348,9 @@ class TypeParam { factory TypeParam.fromJson(Map json) => _$TypeParamFromJson(json); - Map toJson() => _$TypeParamToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class JavaDocComment { JavaDocComment({String? comment}) : comment = comment ?? ''; String comment; @@ -371,10 +360,9 @@ class JavaDocComment { factory JavaDocComment.fromJson(Map json) => _$JavaDocCommentFromJson(json); - Map toJson() => _$JavaDocCommentToJson(this); } -@JsonSerializable(explicitToJson: true) +@JsonSerializable(createToJson: false) class Annotation { Annotation( {required this.simpleName, @@ -386,5 +374,4 @@ class Annotation { factory Annotation.fromJson(Map json) => _$AnnotationFromJson(json); - Map toJson() => _$AnnotationToJson(this); } diff --git a/pkgs/jnigen/lib/src/elements/elements.g.dart b/pkgs/jnigen/lib/src/elements/elements.g.dart index e59d1bc3b..68f91d6eb 100644 --- a/pkgs/jnigen/lib/src/elements/elements.g.dart +++ b/pkgs/jnigen/lib/src/elements/elements.g.dart @@ -20,8 +20,8 @@ ClassDecl _$ClassDeclFromJson(Map json) => ClassDecl( const {}, simpleName: json['simpleName'] as String, binaryName: json['binaryName'] as String, - parentName: json['parentName'] as String?, packageName: json['packageName'] as String, + parentName: json['parentName'] as String?, typeParams: (json['typeParams'] as List?) ?.map((e) => TypeParam.fromJson(e as Map)) .toList() ?? @@ -47,36 +47,12 @@ ClassDecl _$ClassDeclFromJson(Map json) => ClassDecl( (json['values'] as List?)?.map((e) => e as String).toList(), ); -Map _$ClassDeclToJson(ClassDecl instance) => { - 'annotations': instance.annotations.map((e) => e.toJson()).toList(), - 'javadoc': instance.javadoc?.toJson(), - 'modifiers': instance.modifiers.toList(), - 'simpleName': instance.simpleName, - 'binaryName': instance.binaryName, - 'parentName': instance.parentName, - 'packageName': instance.packageName, - 'typeParams': instance.typeParams.map((e) => e.toJson()).toList(), - 'methods': instance.methods.map((e) => e.toJson()).toList(), - 'fields': instance.fields.map((e) => e.toJson()).toList(), - 'superclass': instance.superclass?.toJson(), - 'interfaces': instance.interfaces.map((e) => e.toJson()).toList(), - 'hasStaticInit': instance.hasStaticInit, - 'hasInstanceInit': instance.hasInstanceInit, - 'values': instance.values, - }; - TypeUsage _$TypeUsageFromJson(Map json) => TypeUsage( shorthand: json['shorthand'] as String, kind: $enumDecode(_$KindEnumMap, json['kind']), typeJson: json['type'] as Map, ); -Map _$TypeUsageToJson(TypeUsage instance) => { - 'shorthand': instance.shorthand, - 'kind': _$KindEnumMap[instance.kind]!, - 'type': instance.typeJson, - }; - const _$KindEnumMap = { Kind.primitive: 'PRIMITIVE', Kind.typeVariable: 'TYPE_VARIABLE', @@ -90,11 +66,6 @@ PrimitiveType _$PrimitiveTypeFromJson(Map json) => name: json['name'] as String, ); -Map _$PrimitiveTypeToJson(PrimitiveType instance) => - { - 'name': instance.name, - }; - DeclaredType _$DeclaredTypeFromJson(Map json) => DeclaredType( binaryName: json['binaryName'] as String, simpleName: json['simpleName'] as String, @@ -104,21 +75,10 @@ DeclaredType _$DeclaredTypeFromJson(Map json) => DeclaredType( const [], ); -Map _$DeclaredTypeToJson(DeclaredType instance) => - { - 'binaryName': instance.binaryName, - 'simpleName': instance.simpleName, - 'params': instance.params.map((e) => e.toJson()).toList(), - }; - TypeVar _$TypeVarFromJson(Map json) => TypeVar( name: json['name'] as String, ); -Map _$TypeVarToJson(TypeVar instance) => { - 'name': instance.name, - }; - Wildcard _$WildcardFromJson(Map json) => Wildcard( extendsBound: json['extendsBound'] == null ? null @@ -128,19 +88,10 @@ Wildcard _$WildcardFromJson(Map json) => Wildcard( : TypeUsage.fromJson(json['superBound'] as Map), ); -Map _$WildcardToJson(Wildcard instance) => { - 'extendsBound': instance.extendsBound?.toJson(), - 'superBound': instance.superBound?.toJson(), - }; - ArrayType _$ArrayTypeFromJson(Map json) => ArrayType( type: TypeUsage.fromJson(json['type'] as Map), ); -Map _$ArrayTypeToJson(ArrayType instance) => { - 'type': instance.type.toJson(), - }; - Method _$MethodFromJson(Map json) => Method( annotations: (json['annotations'] as List?) ?.map((e) => Annotation.fromJson(e as Map)) @@ -166,16 +117,6 @@ Method _$MethodFromJson(Map json) => Method( TypeUsage.fromJson(json['returnType'] as Map), ); -Map _$MethodToJson(Method instance) => { - 'annotations': instance.annotations.map((e) => e.toJson()).toList(), - 'javadoc': instance.javadoc?.toJson(), - 'modifiers': instance.modifiers.toList(), - 'name': instance.name, - 'typeParams': instance.typeParams.map((e) => e.toJson()).toList(), - 'params': instance.params.map((e) => e.toJson()).toList(), - 'returnType': instance.returnType.toJson(), - }; - Param _$ParamFromJson(Map json) => Param( annotations: (json['annotations'] as List?) ?.map((e) => Annotation.fromJson(e as Map)) @@ -188,13 +129,6 @@ Param _$ParamFromJson(Map json) => Param( type: TypeUsage.fromJson(json['type'] as Map), ); -Map _$ParamToJson(Param instance) => { - 'annotations': instance.annotations.map((e) => e.toJson()).toList(), - 'javadoc': instance.javadoc?.toJson(), - 'name': instance.name, - 'type': instance.type.toJson(), - }; - Field _$FieldFromJson(Map json) => Field( annotations: (json['annotations'] as List?) ?.map((e) => Annotation.fromJson(e as Map)) @@ -212,15 +146,6 @@ Field _$FieldFromJson(Map json) => Field( defaultValue: json['defaultValue'], ); -Map _$FieldToJson(Field instance) => { - 'annotations': instance.annotations.map((e) => e.toJson()).toList(), - 'javadoc': instance.javadoc?.toJson(), - 'modifiers': instance.modifiers.toList(), - 'name': instance.name, - 'type': instance.type.toJson(), - 'defaultValue': instance.defaultValue, - }; - TypeParam _$TypeParamFromJson(Map json) => TypeParam( name: json['name'] as String, bounds: (json['bounds'] as List?) @@ -229,21 +154,11 @@ TypeParam _$TypeParamFromJson(Map json) => TypeParam( const [], ); -Map _$TypeParamToJson(TypeParam instance) => { - 'name': instance.name, - 'bounds': instance.bounds.map((e) => e.toJson()).toList(), - }; - JavaDocComment _$JavaDocCommentFromJson(Map json) => JavaDocComment( comment: json['comment'] as String?, ); -Map _$JavaDocCommentToJson(JavaDocComment instance) => - { - 'comment': instance.comment, - }; - Annotation _$AnnotationFromJson(Map json) => Annotation( simpleName: json['simpleName'] as String, binaryName: json['binaryName'] as String, @@ -252,10 +167,3 @@ Annotation _$AnnotationFromJson(Map json) => Annotation( ) ?? const {}, ); - -Map _$AnnotationToJson(Annotation instance) => - { - 'simpleName': instance.simpleName, - 'binaryName': instance.binaryName, - 'properties': instance.properties, - }; diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart index e372d4534..b70cf8ca8 100644 --- a/pkgs/jnigen/lib/src/generate_bindings.dart +++ b/pkgs/jnigen/lib/src/generate_bindings.dart @@ -51,23 +51,20 @@ Future generateJniBindings(Config config) async { final androidConfig = config.androidSdkConfig; if (androidConfig != null && androidConfig.addGradleDeps) { final deps = AndroidSdkTools.getGradleClasspaths( - androidConfig.androidExample ?? '.'); + configRoot: config.configRoot, + androidProject: androidConfig.androidExample ?? '.', + ); extraJars.addAll(deps.map(Uri.file)); } if (androidConfig != null && androidConfig.versions != null) { final versions = androidConfig.versions!; + final androidSdkRoot = + androidConfig.sdkRoot ?? AndroidSdkTools.getAndroidSdkRoot(); final androidJar = await AndroidSdkTools.getAndroidJarPath( - sdkRoot: androidConfig.sdkRoot, versionOrder: versions); + sdkRoot: androidSdkRoot, versionOrder: versions); if (androidJar != null) { extraJars.add(Uri.directory(androidJar)); } - if (androidConfig.includeSources) { - final androidSources = await AndroidSdkTools.getAndroidSourcesPath( - sdkRoot: androidConfig.sdkRoot, versionOrder: versions); - if (androidSources != null) { - extraSources.add(Uri.directory(androidSources)); - } - } } summarizer.addSourcePaths(extraSources); diff --git a/pkgs/jnigen/lib/src/logging/logging.dart b/pkgs/jnigen/lib/src/logging/logging.dart index bf4bbe959..561b06b29 100644 --- a/pkgs/jnigen/lib/src/logging/logging.dart +++ b/pkgs/jnigen/lib/src/logging/logging.dart @@ -2,6 +2,8 @@ // 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. +// coverage:ignore-file + import 'dart:io'; import 'package:logging/logging.dart'; @@ -16,11 +18,9 @@ String _colorize(String message, String colorCode) { return message; } -Logger log = Logger('jnigen'); - -/// Set logging level to [level] and initialize the logger. -void setLoggingLevel(Level level) { - Logger.root.level = level; +Logger log = () { + final jnigenLogger = Logger('jnigen'); + Logger.root.level = Level.INFO; Logger.root.onRecord.listen((r) { var message = '(${r.loggerName}) ${r.level.name}: ${r.message}'; if ((r.level == Level.SHOUT || r.level == Level.SEVERE)) { @@ -30,6 +30,15 @@ void setLoggingLevel(Level level) { } stderr.writeln(message); }); + return jnigenLogger; +}(); + +/// Set logging level to [level]. +void setLoggingLevel(Level level) { + /// This initializes `log` as a side effect, so that level setting we apply + /// is always the last one applied. + log.fine('Set log level: $level'); + Logger.root.level = level; } /// Prints [message] without logging information. @@ -40,7 +49,7 @@ void printError(Object? message) { } extension FatalErrors on Logger { - void fatal(Object? message, {int exitCode = 2}) { + void fatal(Object? message, {int exitCode = 1}) { message = _colorize('Fatal: $message', _ansiRed); stderr.writeln(message); exit(exitCode); diff --git a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart index e62d1e8e4..703db1387 100644 --- a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart +++ b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart @@ -7,13 +7,34 @@ import 'package:path/path.dart'; import 'package:jnigen/src/logging/logging.dart'; +class _AndroidToolsException implements Exception { + _AndroidToolsException(this.message); + String message; + @override + String toString() => message; +} + +class SdkNotFoundException extends _AndroidToolsException { + SdkNotFoundException(String message) : super(message); +} + +class GradleException extends _AndroidToolsException { + GradleException(String message) : super(message); +} + class AndroidSdkTools { - /// get path for android API sources - static Future _getVersionDir( - String relative, String? sdkRoot, List versionOrder) async { - if (sdkRoot == null) { - throw ArgumentError('SDK Root not provided'); + static String getAndroidSdkRoot() { + final envVar = Platform.environment['ANDROID_SDK_ROOT']; + if (envVar == null) { + throw SdkNotFoundException('Android SDK not found. Please set ' + 'ANDROID_SDK_ROOT environment variable or specify through command ' + 'line override.'); } + return envVar; + } + + static Future _getVersionDir( + String relative, String sdkRoot, List versionOrder) async { final parent = join(sdkRoot, relative); for (var version in versionOrder) { final dir = Directory(join(parent, 'android-$version')); @@ -24,15 +45,8 @@ class AndroidSdkTools { return null; } - static Future getAndroidSourcesPath( - {String? sdkRoot, required List versionOrder}) async { - final dir = await _getVersionDir('sources', sdkRoot, versionOrder); - log.info('Found sources at $dir'); - return dir; - } - - static Future _getFile(String relative, String file, String? sdkRoot, - List versionOrder) async { + static Future _getFile(String sdkRoot, String relative, + List versionOrder, String file) async { final platform = await _getVersionDir(relative, sdkRoot, versionOrder); if (platform == null) return null; final filePath = join(platform, file); @@ -44,8 +58,8 @@ class AndroidSdkTools { } static Future getAndroidJarPath( - {String? sdkRoot, required List versionOrder}) async => - await _getFile('platforms', 'android.jar', sdkRoot, versionOrder); + {required String sdkRoot, required List versionOrder}) async => + await _getFile(sdkRoot, 'platforms', versionOrder, 'android.jar'); static const _gradleListDepsFunction = ''' task listDependencies(type: Copy) { @@ -71,8 +85,12 @@ task listDependencies(type: Copy) { /// /// If current project is not directly buildable by gradle, eg: a plugin, /// a relative path to other project can be specified using [androidProject]. - static List getGradleClasspaths([String androidProject = '.']) { + static List getGradleClasspaths( + {Uri? configRoot, String androidProject = '.'}) { log.info('trying to obtain gradle classpaths...'); + if (configRoot != null) { + androidProject = join(configRoot.toFilePath(), androidProject); + } final android = join(androidProject, 'android'); final buildGradle = join(android, 'build.gradle'); final buildGradleOld = join(android, 'build.gradle.old'); @@ -96,9 +114,10 @@ task listDependencies(type: Copy) { if (procRes.exitCode != 0) { final inAndroidProject = (androidProject == '.') ? '' : ' in $androidProject'; - throw Exception('\n\ngradle exited with exit code ${procRes.exitCode}\n' - 'This can be related to a known issue with gradle. Please run ' - '`flutter build apk`$inAndroidProject and try again\n'); + throw GradleException('\n\ngradle exited with exit code ' + '${procRes.exitCode}\n. This can be related to a known issue with ' + 'gradle. Please run `flutter build apk`$inAndroidProject and try ' + 'again\n'); } final classpaths = (procRes.stdout as String) .trim() diff --git a/pkgs/jnigen/lib/src/tools/build_summarizer.dart b/pkgs/jnigen/lib/src/tools/build_summarizer.dart index d2dd22d02..38e395eca 100644 --- a/pkgs/jnigen/lib/src/tools/build_summarizer.dart +++ b/pkgs/jnigen/lib/src/tools/build_summarizer.dart @@ -2,6 +2,13 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// TODO(#43): Address concurrently building summarizer. +// +// In the current state summarizer has to be built before tests, which run +// concurrently. Ignoring coverage for this file until that issue is addressed. +// +// coverage:ignore-file + import 'dart:io'; import 'package:path/path.dart'; diff --git a/pkgs/jnigen/lib/src/tools/maven_tools.dart b/pkgs/jnigen/lib/src/tools/maven_tools.dart index 8d1ccd917..07ce7644a 100644 --- a/pkgs/jnigen/lib/src/tools/maven_tools.dart +++ b/pkgs/jnigen/lib/src/tools/maven_tools.dart @@ -75,24 +75,6 @@ class MavenTools { await tempDir.delete(recursive: true); } - /// Get classpath string using JARs in maven's local repository. - static Future getMavenClassPath(List deps) async { - final tempDir = await currentDir.createTemp("maven_temp_"); - final tempClassPath = join(tempDir.path, "maven_classpath"); - await _runMavenCommand( - deps, - [ - 'dependency:build-classpath', - '-Dmdep.outputFile=$tempClassPath', - ], - tempDir); - final classPathFile = File(tempClassPath); - final classpath = await classPathFile.readAsString(); - await classPathFile.delete(); - await tempDir.delete(recursive: true); - return classpath; - } - static String _getStubPom(List deps, {String javaVersion = '11'}) { final depDecls = []; diff --git a/pkgs/jnigen/lib/src/util/command_output.dart b/pkgs/jnigen/lib/src/util/command_output.dart index 64deac2b1..1e7a9e058 100644 --- a/pkgs/jnigen/lib/src/util/command_output.dart +++ b/pkgs/jnigen/lib/src/util/command_output.dart @@ -4,13 +4,5 @@ import 'dart:convert'; -Stream commandOutputStream( - String Function(String) lineMapper, Stream> input) => - input.transform(Utf8Decoder()).transform(LineSplitter()).map(lineMapper); - -Stream prefixedCommandOutputStream( - String prefix, Stream> input) => - commandOutputStream((line) => '$prefix $line', input); - void collectOutputStream(Stream> stream, StringBuffer buffer) => stream.transform(Utf8Decoder()).forEach(buffer.write); diff --git a/pkgs/jnigen/lib/src/util/find_package.dart b/pkgs/jnigen/lib/src/util/find_package.dart index 5c1650c0f..89d95ddc0 100644 --- a/pkgs/jnigen/lib/src/util/find_package.dart +++ b/pkgs/jnigen/lib/src/util/find_package.dart @@ -44,5 +44,3 @@ Future isPackageModifiedAfter(String packageName, DateTime time, } return false; } - -Future findPackageJni() => findPackageRoot('jni'); diff --git a/pkgs/jnigen/pubspec.yaml b/pkgs/jnigen/pubspec.yaml index 34401ce3d..54e2b36f6 100644 --- a/pkgs/jnigen/pubspec.yaml +++ b/pkgs/jnigen/pubspec.yaml @@ -11,7 +11,7 @@ environment: sdk: '>=2.17.0 <3.0.0' dependencies: - json_annotation: ^4.6.0 + json_annotation: ^4.7.0 package_config: ^2.1.0 path: ^1.8.0 args: ^2.3.0 @@ -24,5 +24,5 @@ dev_dependencies: path: ../jni test: ^1.17.5 build_runner: ^2.2.0 - json_serializable: ^6.3.1 + json_serializable: ^6.5.4 diff --git a/pkgs/jnigen/test/config_test.dart b/pkgs/jnigen/test/config_test.dart index 3dcff1ccb..883bae8a2 100644 --- a/pkgs/jnigen/test/config_test.dart +++ b/pkgs/jnigen/test/config_test.dart @@ -52,8 +52,6 @@ void expectConfigsAreEqual(Config a, Config b) { expect(ba, isNotNull, reason: "androidSdkConfig"); expect(aa.versions, ba!.versions, reason: "androidSdkConfig.versions"); expect(aa.sdkRoot, ba.sdkRoot, reason: "androidSdkConfig.sdkRoot"); - expect(aa.includeSources, ba.includeSources, - reason: "androidSdkConfig.includeSources"); } else { expect(ba, isNull, reason: "androidSdkConfig"); } @@ -71,10 +69,32 @@ void expectConfigsAreEqual(Config a, Config b) { } } +final jnigenYaml = join(jacksonCoreTests, 'jnigen.yaml'); + +Config parseYamlConfig({List overrides = const []}) => + Config.parseArgs(['--config', jnigenYaml, ...overrides]); + +void testForErrorChecking( + {required String name, + required List overrides, + dynamic Function(Config)? function}) { + test(name, () { + expect( + () { + final config = parseYamlConfig(overrides: overrides); + if (function != null) { + function(config); + } + }, + throwsA(isA()), + ); + }); +} + void main() { final config = Config.parseArgs([ '--config', - join(jacksonCoreTests, 'jnigen.yaml'), + jnigenYaml, '-Doutput.c.path=$testSrc/', '-Doutput.dart.path=$testLib/', ]); @@ -82,4 +102,23 @@ void main() { test('compare configuration values', () { expectConfigsAreEqual(config, getConfig(root: join(thirdParty, 'test_'))); }); + + group('Test for config error checking', () { + testForErrorChecking( + name: 'Invalid bindings type', + overrides: ['-Doutput.bindings_type=c_base'], + ); + testForErrorChecking( + name: 'Invalid output structure', + overrides: ['-Doutput.dart.structure=singl_file'], + ); + testForErrorChecking( + name: 'Dart path not ending with /', + overrides: ['-Doutput.dart.path=lib'], + ); + testForErrorChecking( + name: 'Invalid log level', + overrides: ['-Dlog_level=inf'], + ); + }); } diff --git a/pkgs/jnigen/test/jackson_core_test/jnigen.yaml b/pkgs/jnigen/test/jackson_core_test/jnigen.yaml index 1cfeb5619..54c90ba3f 100644 --- a/pkgs/jnigen/test/jackson_core_test/jnigen.yaml +++ b/pkgs/jnigen/test/jackson_core_test/jnigen.yaml @@ -1,13 +1,13 @@ maven_downloads: source_deps: - 'com.fasterxml.jackson.core:jackson-core:2.13.4' - source_dir: test/jackson_core_test/third_party/java - jar_dir: test/jackson_core_test/third_party/jar + source_dir: third_party/java/ + jar_dir: third_party/jar/ output: bindings_type: dart_only dart: - path: test/jackson_core_test/third_party/lib/ + path: third_party/lib/ classes: - 'com.fasterxml.jackson.core.JsonFactory' diff --git a/pkgs/jnigen/test/regenerate_examples_test.dart b/pkgs/jnigen/test/regenerate_examples_test.dart new file mode 100644 index 000000000..27dcdced0 --- /dev/null +++ b/pkgs/jnigen/test/regenerate_examples_test.dart @@ -0,0 +1,56 @@ +// 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. + +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:path/path.dart' hide equals; + +import 'package:jnigen/jnigen.dart'; +import 'package:jnigen/tools.dart'; + +import 'test_util/test_util.dart'; + +final inAppJava = join('example', 'in_app_java'); +final inAppJavaYaml = join(inAppJava, 'jnigen.yaml'); +final notificationPlugin = join('example', 'notification_plugin'); +final notificationPluginYaml = join(notificationPlugin, 'jnigen.yaml'); + +/// Generates bindings using jnigen config in [exampleName] and compares +/// them to provided reference outputs. +/// +/// [dartOutput] and [cOutput] are relative paths from example project dir. +void testExample(String exampleName, String dartOutput, String? cOutput) { + test('Generate and compare bindings for $exampleName', () async { + final examplePath = join('example', exampleName); + final configPath = join(examplePath, 'jnigen.yaml'); + + final dartBindingsPath = join(examplePath, dartOutput); + String? cBindingsPath; + if (cOutput != null) { + cBindingsPath = join(examplePath, cOutput); + } + + final config = Config.parseArgs(['--config', configPath]); + try { + await generateAndCompareBindings(config, dartBindingsPath, cBindingsPath); + } on GradleException catch (_) { + stderr.writeln('Skip: $exampleName'); + } + }); +} + +void main() { + testExample( + 'in_app_java', + join('lib', 'android_utils.dart'), + join('src', 'android_utils'), + ); + testExample('pdfbox_plugin', join('lib', 'src', 'third_party'), 'src'); + testExample( + 'notification_plugin', + join('lib', 'notifications.dart'), + 'src', + ); +} diff --git a/pkgs/jnigen/test/test_util/test_util.dart b/pkgs/jnigen/test/test_util/test_util.dart index 27fa75bbc..df6b1b9e8 100644 --- a/pkgs/jnigen/test/test_util/test_util.dart +++ b/pkgs/jnigen/test/test_util/test_util.dart @@ -76,8 +76,15 @@ Future _generateTempBindings(Config config, Directory tempDir) async { await generateJniBindings(config); } -Future generateAndCompareBindings( - Config config, String dartPath, String cPath) async { +/// Generates and compares bindings with reference bindings. +/// +/// [dartReferenceBindings] can be directory or file depending on output +/// configuration. +/// +/// If the config generates C code, [cReferenceBindings] must be a non-null +/// directory path. +Future generateAndCompareBindings(Config config, + String dartReferenceBindings, String? cReferenceBindings) async { final currentDir = Directory.current; final tempDir = currentDir.createTempSync("jnigen_test_temp"); final tempSrc = tempDir.uri.resolve("src/"); @@ -88,9 +95,9 @@ Future generateAndCompareBindings( : tempDir.uri.resolve("lib/"); try { await _generateTempBindings(config, tempDir); - comparePaths(dartPath, tempLib.toFilePath()); + comparePaths(dartReferenceBindings, tempLib.toFilePath()); if (config.outputConfig.bindingsType == BindingsType.cBased) { - comparePaths(cPath, tempSrc.toFilePath()); + comparePaths(cReferenceBindings!, tempSrc.toFilePath()); } } finally { tempDir.deleteSync(recursive: true);