[jnigen] tools: generate_ide_files.dart and pre_commit_checks.dart (https://github.com/dart-lang/jnigen/issues/86)
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 060f8e4..e583083 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml
@@ -2,6 +2,11 @@ # 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. +# Note: There's a script under `jnigen/tool` which runs a subset of these +# tests on local machine. It's a useful little script for checking the +# code before making a PR. If you add a task here, you might want to add +# the equivalent in that script as well. + name: Dart CI on: @@ -124,7 +129,7 @@ - name: install clang tools run: | sudo apt-get update -y - sudo apt-get install -y clang-format + sudo apt-get install -y clang-format build-essential cmake - run: flutter pub get - name: Check formatting run: flutter format --output=none --set-exit-if-changed . @@ -133,6 +138,10 @@ - name: Check C code formatting using clang-format run: clang-format --dry-run -Werror dartjni.c dartjni.h third_party/*.c third_party/*.h working-directory: ./pkgs/jni/src + - name: verify that tool/generate_ide_files.dart generates a file + run: | + dart run tool/generate_ide_files.dart + ls src/compile_commands.json test_jni: runs-on: ubuntu-latest
diff --git a/pkgs/jni/tool/generate_ide_files.dart b/pkgs/jni/tool/generate_ide_files.dart new file mode 100644 index 0000000..ab44ad8 --- /dev/null +++ b/pkgs/jni/tool/generate_ide_files.dart
@@ -0,0 +1,65 @@ +// 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:args/args.dart'; + +const makefilesGenerator = 'make'; +const ninjaGenerator = 'ninja'; + +const cmakeGeneratorNames = { + makefilesGenerator: 'Unix Makefiles', + ninjaGenerator: 'Ninja', +}; + +void runCommand(String exec, List<String> args, String workingDirectory) { + stderr.writeln('+ $exec ${args.join(" ")}'); + final process = + Process.runSync(exec, args, workingDirectory: workingDirectory); + if (process.exitCode != 0) { + stdout.writeln(process.stdout); + stderr.writeln(process.stderr); + throw "command failed with exit code ${process.exitCode}"; + } +} + +void main(List<String> arguments) { + final argParser = ArgParser() + ..addOption( + "generator", + abbr: 'G', + help: 'Generator to pass to CMake. (Either "ninja" or "make").', + allowed: [ninjaGenerator, makefilesGenerator], + defaultsTo: Platform.isWindows ? ninjaGenerator : makefilesGenerator, + ) + ..addFlag('help', abbr: 'h', help: 'Show this usage information.'); + final argResults = argParser.parse(arguments); + if (argResults.rest.isNotEmpty || argResults['help']) { + stderr.writeln('This script generates compile_commands.json for ' + 'C source files in src/'); + stderr.writeln(argParser.usage); + exitCode = 1; + return; + } + final generator = cmakeGeneratorNames[argResults['generator']]; + final tempDir = Directory.current.createTempSync("clangd_setup_temp_"); + final src = Directory.current.uri.resolve("src/"); + try { + runCommand( + "cmake", + [ + "-DCMAKE_EXPORT_COMPILE_COMMANDS=1", + src.toFilePath(), + "-G", + generator!, + ], + tempDir.path); + final createdFile = tempDir.uri.resolve("compile_commands.json"); + final target = src.resolve("compile_commands.json"); + File.fromUri(createdFile).renameSync(target.toFilePath()); + } finally { + tempDir.deleteSync(recursive: true); + } +}
diff --git a/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart b/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart index 992c30b..9ed7429 100644 --- a/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart +++ b/pkgs/jnigen/test/jackson_core_test/generated_files_test.dart
@@ -14,7 +14,7 @@ final lib = join(thirdPartyDir, 'lib'); final src = join(thirdPartyDir, 'src'); await generateAndCompareBindings(getConfig(), lib, src); - }); + }, timeout: Timeout.factor(2)); test( 'generate and analyze bindings for complete library, '
diff --git a/pkgs/jnigen/tool/pre_commit_checks.dart b/pkgs/jnigen/tool/pre_commit_checks.dart new file mode 100644 index 0000000..2c7e966 --- /dev/null +++ b/pkgs/jnigen/tool/pre_commit_checks.dart
@@ -0,0 +1,215 @@ +// 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. + +// This is a script to run a subset of CI checks on development machine before +// submitting PR. This prevents from accidentally forgetting to format or +// remove an unused import. + +// This is rough around the edges. This script may give false positives such +// as some gitignore'd temporary files failing the comparison. + +// On windows, please install 'diffutils' using your favorite package installer +// for this script to work correctly. Also ensure that clang-format is on your +// PATH. + +import 'dart:async'; +import 'dart:io'; + +const ansiRed = '\x1b[31m'; +const ansiDefault = '\x1b[39;49m'; + +void printError(Object? message) { + if (stderr.supportsAnsiEscapes) { + message = '$ansiRed$message$ansiDefault'; + } + stderr.writeln(message); +} + +class StepFailure implements Exception { + StepFailure(this.name); + String name; + @override + String toString() => 'step failed: $name'; +} + +abstract class Step { + /// Runs this step, raises an exception if something fails. + Future<void> run(); +} + +class Callback implements Step { + Callback(this.name, this.function); + String name; + Future<void> Function() function; + @override + Future<void> run() => function(); +} + +class Command implements Step { + Command(this.exec, this.args, this.workingDirectory); + final String exec; + final List<String> args; + final String workingDirectory; + + @override + Future<void> run() async { + final result = + await Process.run(exec, args, workingDirectory: workingDirectory); + if (result.exitCode != 0) { + printError(result.stdout); + printError(result.stderr); + final commandString = "$exec ${args.join(" ")}"; + stderr.writeln("failure executing command: $commandString"); + throw StepFailure(commandString); + } + } +} + +class Runner { + static final gitRoot = getRepositoryRoot(); + Runner(this.name, this.defaultWorkingDir); + String name; + String defaultWorkingDir; + final steps = <Step>[]; + final cleanupSteps = <Step>[]; + + void chainCommand(String exec, List<String> args, + {String? workingDirectory}) => + _addCommand(steps, exec, args, workingDirectory: workingDirectory); + + void chainCleanupCommand(String exec, List<String> args, + {String? workingDirectory}) => + _addCommand(cleanupSteps, exec, args, workingDirectory: workingDirectory); + + void _addCommand(List<Step> list, String exec, List<String> args, + {String? workingDirectory}) { + final resolvedWorkingDirectory = + gitRoot.resolve(workingDirectory ?? defaultWorkingDir); + list.add(Command(exec, args, resolvedWorkingDirectory.toFilePath())); + } + + void chainCallback(String name, Future<void> Function() callback) { + steps.add(Callback(name, callback)); + } + + Future<void> run() async { + stderr.writeln("started: $name"); + var error = false; + for (var step in steps) { + try { + await step.run(); + } on StepFailure catch (e) { + stderr.writeln(e); + error = true; + exitCode = 1; + break; + } + } + stderr.writeln('${error ? "failed" : "complete"}: $name'); + for (var step in cleanupSteps) { + try { + await step.run(); + } on Exception catch (e) { + printError("ERROR: $e"); + } + } + } +} + +Uri getRepositoryRoot() { + final gitCommand = Process.runSync("git", ["rev-parse", "--show-toplevel"]); + final output = gitCommand.stdout as String; + return Uri.directory(output.trim()); +} + +void main() async { + final jniAnalyze = Runner("Analyze JNI", "jni"); + jniAnalyze + ..chainCommand("dart", ["analyze", "--fatal-infos"]) + ..chainCommand( + "dart", ["format", "--output=none", "--set-exit-if-changed", "."]) + ..chainCommand( + "clang-format", + [ + "--dry-run", + "-Werror", + "dartjni.c", + "dartjni.h", + "third_party/global_jni_env.c", + "third_party/global_jni_env.h", + ], + workingDirectory: "jni/src"); + final jniTest = Runner("Test JNI", "jni") + ..chainCommand("dart", ["run", "jni:setup"]) + ..chainCommand("dart", ["test", "-j", "1"]); + unawaited(jniAnalyze.run().then((f) => jniTest.run())); + final ffigenBindingsPath = getRepositoryRoot() + .resolve("jni/lib/src/third_party/jni_bindings_generated.dart"); + final ffigenBindings = File.fromUri(ffigenBindingsPath); + final oldBindingsText = ffigenBindings.readAsStringSync(); + final ffigenCompare = Runner("Generate & Compare FFIGEN bindings", "jni") + ..chainCommand("dart", ["run", "ffigen", "--config", "ffigen.yaml"]) + ..chainCallback("compare bindings", () async { + final newBindingsText = await ffigenBindings.readAsString(); + if (newBindingsText != oldBindingsText) { + await ffigenBindings.writeAsString(oldBindingsText); + throw "new JNI.h bindings differ from old bindings"; + } + }); + unawaited(ffigenCompare.run()); + + final jnigenAnalyze = Runner("Analyze jnigen", "jnigen") + ..chainCommand("dart", ["analyze", "--fatal-infos"]) + ..chainCommand( + "dart", ["format", "--output=none", "--set-exit-if-changed", "."]) + ..chainCommand("dart", ["run", "jnigen:setup"]); + final jnigenTest = Runner("Test jnigen", "jnigen") + ..chainCommand("dart", ["test"]); + final compareInAppJavaBindings = Runner( + "Generate & compare InAppJava bindings", "jnigen/example/in_app_java") + ..chainCommand("dart", [ + "run", + "jnigen", + "--config", + "jnigen.yaml", + "-Dc_root=src_temp", + "-Ddart_root=lib_temp", + ]) + ..chainCommand("diff", ["-qr", "lib/android_utils/", "lib_temp/"]) + ..chainCommand("diff", ["-qr", "src/android_utils/", "src_temp/"]) + ..chainCleanupCommand("rm", ["-r", "lib_temp", "src_temp"]); + final comparePdfboxBindings = Runner( + "Generate & compare PdfBox Bindings", "jnigen/example/pdfbox_plugin") + ..chainCommand("dart", [ + "run", + "jnigen", + "--config", + "jnigen.yaml", + "-Dc_root=src_temp", + "-Ddart_root=lib_temp", + ]) + ..chainCommand("diff", ["-qr", "lib/third_party/", "lib_temp/"]) + ..chainCommand("diff", ["-qr", "src/", "src_temp/"]) + ..chainCleanupCommand("rm", ["-r", "lib_temp", "src_temp"]); + final compareNotificationPluginBindings = Runner( + "Generate & compare NotificationPlugin Bindings", + "jnigen/example/notification_plugin") + ..chainCommand("dart", [ + "run", + "jnigen", + "--config", + "jnigen.yaml", + "-Dc_root=src_temp", + "-Ddart_root=lib_temp", + ]) + ..chainCommand("diff", ["-qr", "lib/", "lib_temp/"]) + ..chainCommand("diff", ["-qr", "src/", "src_temp/"]) + ..chainCleanupCommand("rm", ["-r", "lib_temp", "src_temp"]); + unawaited(jnigenAnalyze.run().then((_) { + jnigenTest.run(); + compareInAppJavaBindings.run(); + comparePdfboxBindings.run(); + compareNotificationPluginBindings.run(); + })); +}