[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();
+  }));
+}