[jnigen] Make jni:setup build all native libraries (https://github.com/dart-lang/jnigen/issues/96)

diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index a578787..554577f 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -404,7 +404,7 @@
       - name: Run standalone example
         run: |
           dart pub get
-          dart run jni:setup && dart run jni:setup -p pdfbox_plugin
+          dart run jni:setup
           wget 'https://dart.dev/guides/language/specifications/DartLangSpec-v2.2.pdf'
           dart run bin/pdf_info.dart DartLangSpec-v2.2.pdf
         working-directory: ./pkgs/jnigen/example/pdfbox_plugin/dart_example
diff --git a/pkgs/jni/bin/setup.dart b/pkgs/jni/bin/setup.dart
index 8f12c6b..23226c3 100644
--- a/pkgs/jni/bin/setup.dart
+++ b/pkgs/jni/bin/setup.dart
@@ -10,11 +10,14 @@
 const ansiRed = '\x1b[31m';
 const ansiDefault = '\x1b[39;49m';
 
+const jniNativeBuildDirective =
+    '# jni_native_build (Build with jni:setup. Do not delete this line.)';
+
 const _defaultRelativeBuildPath = "build/jni_libs";
 
 const _buildPath = "build-path";
-const _srcPath = "source-path";
-const _packageName = 'package-name';
+const _srcPath = "add-source";
+const _packageName = 'add-package';
 const _verbose = "verbose";
 const _cmakeArgs = "cmake-args";
 
@@ -65,13 +68,14 @@
 class Options {
   Options(ArgResults arg)
       : buildPath = arg[_buildPath],
-        srcPath = arg[_srcPath],
-        packageName = arg[_packageName] ?? 'jni',
+        sources = arg[_srcPath],
+        packages = arg[_packageName],
         cmakeArgs = arg[_cmakeArgs],
         verbose = arg[_verbose] ?? false;
 
-  String? buildPath, srcPath;
-  String packageName;
+  String? buildPath;
+  List<String> sources;
+  List<String> packages;
   List<String> cmakeArgs;
   bool verbose;
 }
@@ -88,18 +92,40 @@
 ///
 /// It's assumed C FFI sources are in "src/" relative to package root.
 /// If package cannot be found, null is returned.
-Future<String?> findSources(String packageName) async {
+Future<String> findSources(String packageName) async {
   final packageConfig = await findPackageConfig(Directory.current);
   if (packageConfig == null) {
-    return null;
+    throw UnsupportedError("Please run from project root.");
   }
-  final package = packageConfig[options.packageName];
+  final package = packageConfig[packageName];
   if (package == null) {
-    return null;
+    throw UnsupportedError("Cannot find package: $packageName");
   }
   return package.root.resolve("src").toFilePath();
 }
 
+/// Return '/src' directories of all dependencies which has a CMakeLists.txt
+/// file.
+Future<Map<String, String>> findDependencySources() async {
+  final packageConfig = await findPackageConfig(Directory.current);
+  if (packageConfig == null) {
+    throw UnsupportedError("Please run the command from project root.");
+  }
+  final sources = <String, String>{};
+  for (var package in packageConfig.packages) {
+    final src = package.root.resolve("src/");
+    final cmakeLists = src.resolve("CMakeLists.txt");
+    final cmakeListsFile = File.fromUri(cmakeLists);
+    if (cmakeListsFile.existsSync()) {
+      final firstLine = cmakeListsFile.readAsLinesSync().first;
+      if (firstLine == jniNativeBuildDirective) {
+        sources[package.name] = src.toFilePath();
+      }
+    }
+  }
+  return sources;
+}
+
 /// Returns true if [artifact] does not exist, or any file in [sourceDir] is
 /// newer than [artifact].
 bool needsBuild(File artifact, Directory sourceDir) {
@@ -129,13 +155,12 @@
   final parser = ArgParser()
     ..addOption(_buildPath,
         abbr: 'b', help: 'Directory to place built artifacts')
-    ..addOption(_srcPath,
+    ..addMultiOption(_srcPath,
         abbr: 's', help: 'alternative path to package:jni sources')
-    ..addOption(_packageName,
+    ..addMultiOption(_packageName,
         abbr: 'p',
         help: 'package for which native'
-            'library should be built',
-        defaultsTo: 'jni')
+            'library should be built')
     ..addFlag(_verbose, abbr: 'v', help: 'Enable verbose output')
     ..addMultiOption(_cmakeArgs,
         abbr: 'm', help: 'Pass additional argument to CMake');
@@ -151,69 +176,73 @@
     return;
   }
 
-  final srcPath = options.srcPath ?? await findSources(options.packageName);
-
-  if (srcPath == null) {
-    stderr.writeln("Cannot find sources for package ${options.packageName} "
-        "and no sources were manually specified.");
+  final sources = options.sources;
+  for (var packageName in options.packages) {
+    sources.add(await findSources(packageName));
+  }
+  if (sources.isEmpty) {
+    final dependencySources = await findDependencySources();
+    stderr.writeln("selecting source directories for dependencies: "
+        "${dependencySources.keys}");
+    sources.addAll(dependencySources.values);
+  } else {
+    stderr.writeln("selecting source directories: $sources");
+  }
+  if (sources.isEmpty) {
+    stderr.writeln('No source paths to build!');
     exitCode = 1;
     return;
   }
-
-  final srcDir = Directory(srcPath);
-  if (!srcDir.existsSync()) {
-    stderr.writeln('Directory $srcPath does not exist');
-    exitCode = 1;
-    return;
-  }
-
-  verboseLog("srcPath: $srcPath");
-
-  final currentDirUri = Uri.directory(".");
-  final buildPath = options.buildPath ??
-      currentDirUri.resolve(_defaultRelativeBuildPath).toFilePath();
-  final buildDir = Directory(buildPath);
-  await buildDir.create(recursive: true);
-  verboseLog("buildPath: $buildPath");
-
-  if (buildDir.absolute.uri == srcDir.absolute.uri) {
-    stderr.writeln("Please build in a directory different than source.");
-    exitCode = 1;
-    return;
-  }
-
-  final targetFileUri = buildDir.uri.resolve(getTargetName(srcDir));
-  final targetFile = File.fromUri(targetFileUri);
-  if (!needsBuild(targetFile, srcDir)) {
-    verboseLog("last modified of ${targetFile.path}: "
-        "${targetFile.lastModifiedSync()}");
-    stderr.writeln("target newer than source, skipping build");
-    return;
-  }
-
-  // Note: creating temp dir in .dart_tool/jni instead of SystemTemp
-  // because latter can fail tests on Windows CI, when system temp is on
-  // separate drive or something.
-  final jniDirUri = Uri.directory(".dart_tool").resolve("jni");
-  final jniDir = Directory.fromUri(jniDirUri);
-  await jniDir.create(recursive: true);
-  final tempDir = await jniDir.createTemp("jni_native_build_");
-  final cmakeArgs = <String>[];
-  cmakeArgs.addAll(options.cmakeArgs);
-  // Pass absolute path of srcDir because cmake command is run in temp dir
-  cmakeArgs.add(srcDir.absolute.path);
-  await runCommand("cmake", cmakeArgs, tempDir.path);
-  await runCommand("cmake", ["--build", "."], tempDir.path);
-  final dllDirUri =
-      Platform.isWindows ? tempDir.uri.resolve("Debug") : tempDir.uri;
-  final dllDir = Directory.fromUri(dllDirUri);
-  for (var entry in dllDir.listSync()) {
-    final dllSuffix = Platform.isWindows ? "dll" : "so";
-    if (entry.path.endsWith(dllSuffix)) {
-      final dllName = entry.uri.pathSegments.last;
-      final target = buildDir.uri.resolve(dllName);
-      entry.renameSync(target.toFilePath());
+  for (var srcPath in sources) {
+    final srcDir = Directory(srcPath);
+    if (!srcDir.existsSync()) {
+      stderr.writeln('Directory $srcPath does not exist');
+      exitCode = 1;
+      return;
     }
+
+    verboseLog("srcPath: $srcPath");
+
+    final currentDirUri = Uri.directory(".");
+    final buildPath = options.buildPath ??
+        currentDirUri.resolve(_defaultRelativeBuildPath).toFilePath();
+    final buildDir = Directory(buildPath);
+    await buildDir.create(recursive: true);
+    verboseLog("buildPath: $buildPath");
+
+    final targetFileUri = buildDir.uri.resolve(getTargetName(srcDir));
+    final targetFile = File.fromUri(targetFileUri);
+    if (!needsBuild(targetFile, srcDir)) {
+      verboseLog("last modified of ${targetFile.path}: "
+          "${targetFile.lastModifiedSync()}");
+      stderr.writeln("target newer than source, skipping build");
+      continue;
+    }
+
+    // Note: creating temp dir in .dart_tool/jni instead of SystemTemp
+    // because latter can fail tests on Windows CI, when system temp is on
+    // separate drive or something.
+    final jniDirUri = Uri.directory(".dart_tool").resolve("jni");
+    final jniDir = Directory.fromUri(jniDirUri);
+    await jniDir.create(recursive: true);
+    final tempDir = await jniDir.createTemp("jni_native_build_");
+    final cmakeArgs = <String>[];
+    cmakeArgs.addAll(options.cmakeArgs);
+    // Pass absolute path of srcDir because cmake command is run in temp dir
+    cmakeArgs.add(srcDir.absolute.path);
+    await runCommand("cmake", cmakeArgs, tempDir.path);
+    await runCommand("cmake", ["--build", "."], tempDir.path);
+    final dllDirUri =
+        Platform.isWindows ? tempDir.uri.resolve("Debug") : tempDir.uri;
+    final dllDir = Directory.fromUri(dllDirUri);
+    for (var entry in dllDir.listSync()) {
+      final dllSuffix = Platform.isWindows ? "dll" : "so";
+      if (entry.path.endsWith(dllSuffix)) {
+        final dllName = entry.uri.pathSegments.last;
+        final target = buildDir.uri.resolve(dllName);
+        entry.renameSync(target.toFilePath());
+      }
+    }
+    await tempDir.delete(recursive: true);
   }
-  await tempDir.delete(recursive: true);
 }
diff --git a/pkgs/jni/lib/jni.dart b/pkgs/jni/lib/jni.dart
index 49485a5..4437e8e 100644
--- a/pkgs/jni/lib/jni.dart
+++ b/pkgs/jni/lib/jni.dart
@@ -30,14 +30,15 @@
 /// This module depends on a shared library written in C. Therefore on dart
 /// standalone:
 ///
-/// * Run `dart run jni:setup` to build the shared library. The default output
-/// directory is build/jni_libs, which can be changed using `-B` switch.
+/// * Run `dart run jni:setup` to build the shared library. This command builds
+/// all dependency libraries with native code (package:jni and jnigen-generated)
+/// libraries if any.
+///
+/// The default output directory is build/jni_libs, which can be changed
+/// using `-B` switch.
 ///
 /// * Provide the location of library to `Jni.spawn` call.
 ///
-/// * If there are generated libraries (using jnigen), also build them using
-/// the command: `dart run jni:setup -p package_name` in the same directory.
-///
 /// ## JNIEnv
 /// `GlobalJniEnv` type provides a thin wrapper over `JNIEnv*` which can be used
 /// from across threads, and always returns JNI global references. This is
diff --git a/pkgs/jni/src/CMakeLists.txt b/pkgs/jni/src/CMakeLists.txt
index 23ac23b..d0c3ee6 100644
--- a/pkgs/jni/src/CMakeLists.txt
+++ b/pkgs/jni/src/CMakeLists.txt
@@ -1,3 +1,5 @@
+# jni_native_build (Build with jni:setup. Do not delete this line.)
+
 # The Flutter tooling requires that developers have CMake 3.10 or later
 # installed. You should not increase this version, as doing so will cause
 # the plugin to fail to compile for some customers of the plugin.
diff --git a/pkgs/jnigen/cmake/CMakeLists.txt.tmpl b/pkgs/jnigen/cmake/CMakeLists.txt.tmpl
index cf7b53d..d0b8a73 100644
--- a/pkgs/jnigen/cmake/CMakeLists.txt.tmpl
+++ b/pkgs/jnigen/cmake/CMakeLists.txt.tmpl
@@ -1,3 +1,5 @@
+# jni_native_build (Build with jni:setup. Do not delete this line.)
+
 # The Flutter tooling requires that developers have CMake 3.10 or later
 # installed. You should not increase this version, as doing so will cause
 # the plugin to fail to compile for some customers of the plugin.
diff --git a/pkgs/jnigen/example/README.md b/pkgs/jnigen/example/README.md
index 821e883..2380dcb 100644
--- a/pkgs/jnigen/example/README.md
+++ b/pkgs/jnigen/example/README.md
@@ -20,7 +20,7 @@
 * Generate JNI bindings by running `dart run jnigen --config jnigen.yaml`.
 
 * In the CLI project which uses this package, add this package, and `jni` as a dependency.
-* Run `dart run jni:setup && dart run jni:setup -p <generated_package_name>` to build native libraries for JNI base library and your package respectively.
+* Run `dart run jni:setup` to build native libraries for JNI base library and jnigen generated package.
 * Import the package. See [pdf_info.dart](pdfbox_plugin/dart_example/bin/pdf_info.dart) for how to use the JNI from dart standalone.
 
 ### Flutter FFI plugin
diff --git a/pkgs/jnigen/example/in_app_java/src/android_utils/CMakeLists.txt b/pkgs/jnigen/example/in_app_java/src/android_utils/CMakeLists.txt
index fe6f925..0c47c28 100644
--- a/pkgs/jnigen/example/in_app_java/src/android_utils/CMakeLists.txt
+++ b/pkgs/jnigen/example/in_app_java/src/android_utils/CMakeLists.txt
@@ -1,3 +1,5 @@
+# jni_native_build (Build with jni:setup. Do not delete this line.)
+
 # The Flutter tooling requires that developers have CMake 3.10 or later
 # installed. You should not increase this version, as doing so will cause
 # the plugin to fail to compile for some customers of the plugin.
diff --git a/pkgs/jnigen/example/notification_plugin/src/CMakeLists.txt b/pkgs/jnigen/example/notification_plugin/src/CMakeLists.txt
index c733845..39c9850 100644
--- a/pkgs/jnigen/example/notification_plugin/src/CMakeLists.txt
+++ b/pkgs/jnigen/example/notification_plugin/src/CMakeLists.txt
@@ -1,3 +1,5 @@
+# jni_native_build (Build with jni:setup. Do not delete this line.)
+
 # The Flutter tooling requires that developers have CMake 3.10 or later
 # installed. You should not increase this version, as doing so will cause
 # the plugin to fail to compile for some customers of the plugin.
diff --git a/pkgs/jnigen/example/pdfbox_plugin/dart_example/README.md b/pkgs/jnigen/example/pdfbox_plugin/dart_example/README.md
index f111493..6a6755e 100644
--- a/pkgs/jnigen/example/pdfbox_plugin/dart_example/README.md
+++ b/pkgs/jnigen/example/pdfbox_plugin/dart_example/README.md
@@ -1,7 +1,7 @@
 ## Running
 After generating `pdfbox_plugin` bindings in the parent directory
 
-* setup native libraries: `dart run jni:setup && dart run jni:setup -p pdfbox_plugin`
+* setup native libraries: `dart run jni:setup`
 
 * `dart run bin/pdf_info.dart <Path_to_PDF_File>`
 
diff --git a/pkgs/jnigen/example/pdfbox_plugin/src/CMakeLists.txt b/pkgs/jnigen/example/pdfbox_plugin/src/CMakeLists.txt
index 4ed5b3d..f353765 100644
--- a/pkgs/jnigen/example/pdfbox_plugin/src/CMakeLists.txt
+++ b/pkgs/jnigen/example/pdfbox_plugin/src/CMakeLists.txt
@@ -1,3 +1,5 @@
+# jni_native_build (Build with jni:setup. Do not delete this line.)
+
 # The Flutter tooling requires that developers have CMake 3.10 or later
 # installed. You should not increase this version, as doing so will cause
 # the plugin to fail to compile for some customers of the plugin.
diff --git a/pkgs/jnigen/test/bindings_test.dart b/pkgs/jnigen/test/bindings_test.dart
index 015c1a3..799add7 100644
--- a/pkgs/jnigen/test/bindings_test.dart
+++ b/pkgs/jnigen/test/bindings_test.dart
@@ -27,11 +27,16 @@
 final simplePackageTestJava = join(simplePackageTest, 'java');
 
 Future<void> setupDylibsAndClasses() async {
-  await runCommand('dart', ['run', 'jni:setup']);
-  await runCommand(
-      'dart', ['run', 'jni:setup', '-s', join(simplePackageTest, 'src')]);
-  await runCommand('dart',
-      ['run', 'jni:setup', '-s', join(jacksonCoreTest, 'third_party', 'src')]);
+  await runCommand('dart', [
+    'run',
+    'jni:setup',
+    '-p',
+    'jni',
+    '-s',
+    join(simplePackageTest, 'src'),
+    '-s',
+    join(jacksonCoreTest, 'third_party', 'src')
+  ]);
   final group = join('com', 'github', 'dart_lang', 'jnigen');
   await runCommand(
       'javac',
diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/src/CMakeLists.txt b/pkgs/jnigen/test/jackson_core_test/third_party/src/CMakeLists.txt
index 475daed..1df24fc 100644
--- a/pkgs/jnigen/test/jackson_core_test/third_party/src/CMakeLists.txt
+++ b/pkgs/jnigen/test/jackson_core_test/third_party/src/CMakeLists.txt
@@ -1,3 +1,5 @@
+# jni_native_build (Build with jni:setup. Do not delete this line.)
+
 # The Flutter tooling requires that developers have CMake 3.10 or later
 # installed. You should not increase this version, as doing so will cause
 # the plugin to fail to compile for some customers of the plugin.
diff --git a/pkgs/jnigen/test/simple_package_test/src/CMakeLists.txt b/pkgs/jnigen/test/simple_package_test/src/CMakeLists.txt
index a029be9..eeb352d 100644
--- a/pkgs/jnigen/test/simple_package_test/src/CMakeLists.txt
+++ b/pkgs/jnigen/test/simple_package_test/src/CMakeLists.txt
@@ -1,3 +1,5 @@
+# jni_native_build (Build with jni:setup. Do not delete this line.)
+
 # The Flutter tooling requires that developers have CMake 3.10 or later
 # installed. You should not increase this version, as doing so will cause
 # the plugin to fail to compile for some customers of the plugin.