[jnigen] Support customizing the command to call ApiSummarizer (#3579)
diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md
index a98c9a6..611739b 100644
--- a/pkgs/jnigen/CHANGELOG.md
+++ b/pkgs/jnigen/CHANGELOG.md
@@ -22,6 +22,8 @@
     the `generate` function.
   - Change remaining `String` paths to `Uri`.
 - Allow interface mixin names to be customized using the visitor API.
+- Support customizing the command `java -jar ApiSummarizer.jar` via the
+  `summarizer.command` field in config.
 
 ## 0.17.0
 
diff --git a/pkgs/jnigen/lib/src/config/config_types.dart b/pkgs/jnigen/lib/src/config/config_types.dart
index 75da56f..e60daec 100644
--- a/pkgs/jnigen/lib/src/config/config_types.dart
+++ b/pkgs/jnigen/lib/src/config/config_types.dart
@@ -326,6 +326,13 @@
   /// disabled.
   final AndroidSdk? androidSdk;
 
+  /// Command used to run the API summarizer.
+  ///
+  /// This should only be used if the system uses a prebuilt `ApiSummarizer.jar`
+  /// file. If provided, building ApiSummarizer using Gradle is skipped and this
+  /// command is used directly to invoke the summarizer.
+  final String? summarizerCommand;
+
   Input({
     this.sourcePath = const [],
     this.classPath = const [],
@@ -335,6 +342,7 @@
     this.backend,
     this.mavenDownloads,
     this.androidSdk,
+    this.summarizerCommand,
   }) : workingDirectory = workingDirectory ?? Uri.directory('.') {
     for (final className in classes) {
       _validateClassName(className);
@@ -459,6 +467,7 @@
         extraArgs: prov.getStringList(_Props.summarizerArgs) ?? const [],
         backend: getSummarizerBackend(prov.getString(_Props.backend), null),
         workingDirectory: prov.getPath(_Props.summarizerWorkingDir),
+        summarizerCommand: prov.getString(_Props.summarizerCommand),
         mavenDownloads: prov.hasValue(_Props.mavenDownloads)
             ? MavenDownloads(
                 sourceDeps: prov.getStringList(_Props.sourceDeps) ?? const [],
@@ -641,6 +650,7 @@
   static const summarizer = 'summarizer';
   static const summarizerArgs = '$summarizer.extra_args';
   static const summarizerWorkingDir = '$summarizer.working_dir';
+  static const summarizerCommand = '$summarizer.command';
   static const backend = '$summarizer.backend';
 
   static const sourcePath = 'source_path';
diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart
index bf6875d..9605d25 100644
--- a/pkgs/jnigen/lib/src/generate_bindings.dart
+++ b/pkgs/jnigen/lib/src/generate_bindings.dart
@@ -45,7 +45,9 @@
       ..addAll(Annotated.defaultNullableAnnotations)
       ..addAll(nullability.nullable);
 
-    await buildSummarizerIfNotExists();
+    if (input.summarizerCommand == null) {
+      await buildSummarizerIfNotExists();
+    }
 
     final Classes classes;
 
diff --git a/pkgs/jnigen/lib/src/summary/summary.dart b/pkgs/jnigen/lib/src/summary/summary.dart
index 55accff..66862d2 100644
--- a/pkgs/jnigen/lib/src/summary/summary.dart
+++ b/pkgs/jnigen/lib/src/summary/summary.dart
@@ -57,7 +57,7 @@
 /// script before this API is used.
 class SummarizerCommand {
   SummarizerCommand({
-    this.command = 'java -jar .dart_tool/jnigen/ApiSummarizer.jar',
+    required this.command,
     List<Uri>? sourcePath,
     List<Uri>? classPath,
     this.extraArgs = const [],
@@ -126,6 +126,8 @@
 
 Future<Classes> getSummary(JniGenerator config) async {
   final summarizer = SummarizerCommand(
+    command: config.input.summarizerCommand ??
+        'java -jar .dart_tool/jnigen/ApiSummarizer.jar',
     sourcePath: config.input.sourcePath,
     classPath: config.input.classPath,
     classes: config.input.classes,
diff --git a/pkgs/jnigen/test/config_test.dart b/pkgs/jnigen/test/config_test.dart
index c22354a..2e9ad8d 100644
--- a/pkgs/jnigen/test/config_test.dart
+++ b/pkgs/jnigen/test/config_test.dart
@@ -55,6 +55,8 @@
   expect(a.input.workingDirectory, b.input.workingDirectory,
       reason: 'workingDirectory');
   expect(a.input.backend, b.input.backend, reason: 'backend');
+  expect(a.input.summarizerCommand, equals(b.input.summarizerCommand),
+      reason: 'summarizerCommand');
   expect(a.imports.symbolFiles, b.imports.symbolFiles,
       reason: 'imports.symbolFiles');
   expect(a.imports.hide, b.imports.hide, reason: 'imports.hide');
diff --git a/pkgs/jnigen/test/generation_test.dart b/pkgs/jnigen/test/generation_test.dart
index 66ac2e2..da55bcf 100644
--- a/pkgs/jnigen/test/generation_test.dart
+++ b/pkgs/jnigen/test/generation_test.dart
@@ -5,9 +5,21 @@
 import 'dart:io';
 
 import 'package:jnigen/jnigen.dart';
+import 'package:jnigen/src/logging/logging.dart';
+import 'package:jnigen/src/summary/summary.dart';
 import 'package:path/path.dart';
 import 'package:test/test.dart';
 
+void expectBuildSummarizerTriggered(List<String> log, {bool triggered = true}) {
+  final matcher = contains(predicate<String>(
+    (msg) =>
+        msg.contains('ApiSummarizer.jar exists') ||
+        msg.contains('Building ApiSummarizer component') ||
+        msg.contains('Rebuilding ApiSummarizer component'),
+  ));
+  expect(log, triggered ? matcher : isNot(matcher));
+}
+
 void main() {
   test('Warn if non-jnigen-generated files exist in directory', () async {
     final root = await Directory.current.createTemp();
@@ -20,6 +32,8 @@
     final sourcePath =
         join(Directory.current.path, 'test', 'simple_package_test', 'java');
     print(sourcePath);
+    final logMessages = <String>[];
+    final logSub = log.onRecord.listen((r) => logMessages.add(r.message));
     await expectLater(
       () => JniGenerator(
         input: Input(
@@ -40,6 +54,55 @@
         predicate((error) => error.toString().contains('non_gen.dart')),
       ])),
     );
+    await logSub.cancel();
+    expectBuildSummarizerTriggered(logMessages, triggered: true);
+    await root.delete(recursive: true);
+  });
+
+  test('buildSummarizerIfNotExists is skipped if summarizerCommand is passed',
+      () async {
+    final root = await Directory.current.createTemp();
+    final fakeSummarizer = File(join(root.path, 'fake_summarizer.dart'));
+    const errorMessage = 'Custom summarizer error output';
+    await fakeSummarizer.writeAsString('''
+import 'dart:io';
+
+void main() {
+  stderr.writeln('$errorMessage');
+  exit(1);
+}
+''');
+    final outDir = Directory(join(root.path, 'out'))..createSync();
+
+    final config = JniGenerator(
+      input: Input(
+        classes: ['com.example.Test'],
+        summarizerCommand: '${Platform.executable} ${fakeSummarizer.path}',
+      ),
+      output: Output(
+        dart: DartCodeOutput(
+          path: outDir.uri,
+          structure: OutputStructure.packageStructure,
+        ),
+      ),
+    );
+
+    final logMessages = <String>[];
+    final logSub = log.onRecord.listen((r) => logMessages.add(r.message));
+    try {
+      await getSummary(config);
+      fail('Expected SummaryParseException');
+    } on SummaryParseException catch (e) {
+      expect(e.stderr, contains(errorMessage));
+    }
+    await logSub.cancel();
+
+    expectBuildSummarizerTriggered(logMessages, triggered: false);
+    expect(
+      logMessages,
+      contains(predicate<String>(
+          (msg) => msg.contains('execute ${Platform.executable}'))),
+    );
     await root.delete(recursive: true);
   });
 }