[jnigen] logging (https://github.com/dart-lang/jnigen/issues/48)

diff --git a/pkgs/jnigen/lib/src/bindings/dart_bindings.dart b/pkgs/jnigen/lib/src/bindings/dart_bindings.dart
index 0f205c2..8367161 100644
--- a/pkgs/jnigen/lib/src/bindings/dart_bindings.dart
+++ b/pkgs/jnigen/lib/src/bindings/dart_bindings.dart
@@ -2,10 +2,9 @@
 // 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 'package:jnigen/src/config/config.dart';
+import 'package:jnigen/src/logging/logging.dart';
 import 'package:jnigen/src/util/rename_conflict.dart';
 
 import 'symbol_resolver.dart';
@@ -41,7 +40,9 @@
     if (!decl.isIncluded) {
       return '';
     }
-    return _class(decl);
+    final bindings = _class(decl);
+    log.finest('generated bindings for class ${decl.binaryName}');
+    return bindings;
   }
 
   String _class(ClassDecl decl) {
@@ -72,7 +73,7 @@
         s.write(_field(decl, field));
         s.writeln();
       } on SkipException catch (e) {
-        stderr.writeln('skip field ${decl.binaryName}#${field.name}: '
+        log.info('skip field ${decl.binaryName}#${field.name}: '
             '${e.message}');
       }
     }
@@ -85,7 +86,7 @@
         s.write(_method(decl, method));
         s.writeln();
       } on SkipException catch (e) {
-        stderr.writeln('skip field ${decl.binaryName}#${method.name}: '
+        log.info('skip field ${decl.binaryName}#${method.name}: '
             '${e.message}');
       }
     }
diff --git a/pkgs/jnigen/lib/src/bindings/preprocessor.dart b/pkgs/jnigen/lib/src/bindings/preprocessor.dart
index dabe584..61c2141 100644
--- a/pkgs/jnigen/lib/src/bindings/preprocessor.dart
+++ b/pkgs/jnigen/lib/src/bindings/preprocessor.dart
@@ -2,11 +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.
 
-import 'dart:io';
-
 import 'package:jnigen/src/elements/elements.dart';
 import 'package:jnigen/src/config/config.dart';
+import 'package:jnigen/src/logging/logging.dart';
 import 'package:jnigen/src/util/rename_conflict.dart';
+
 import 'common.dart';
 
 /// Preprocessor which fills information needed by both Dart and C generators.
@@ -22,7 +22,7 @@
     if (decl.isPreprocessed) return;
     if (!_isClassIncluded(decl, config)) {
       decl.isIncluded = false;
-      stdout.writeln('exclude class ${decl.binaryName}');
+      log.info('exclude class ${decl.binaryName}');
       decl.isPreprocessed = true;
       return;
     }
@@ -37,11 +37,12 @@
         decl.nameCounts.addAll(superclass.nameCounts);
       }
     }
-
+    log.finest('Superclass of ${decl.binaryName} resolved to '
+        '${superclass?.binaryName}');
     for (var field in decl.fields) {
       if (!_isFieldIncluded(decl, field, config)) {
         field.isIncluded = false;
-        stderr.writeln('exclude ${decl.binaryName}#${field.name}');
+        log.info('exclude ${decl.binaryName}#${field.name}');
         continue;
       }
       field.finalName = renameConflict(decl.nameCounts, field.name);
@@ -50,7 +51,7 @@
     for (var method in decl.methods) {
       if (!_isMethodIncluded(decl, method, config)) {
         method.isIncluded = false;
-        stderr.writeln('exclude method ${decl.binaryName}#${method.name}');
+        log.info('exclude method ${decl.binaryName}#${method.name}');
         continue;
       }
       var realName = method.name;
@@ -76,6 +77,7 @@
       }
     }
     decl.isPreprocessed = true;
+    log.finest('preprocessed ${decl.binaryName}');
   }
 
   static bool _isFieldIncluded(ClassDecl decl, Field field, Config config) =>
diff --git a/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart b/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart
index 530e570..45b9e1a 100644
--- a/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart
+++ b/pkgs/jnigen/lib/src/bindings/symbol_resolver.dart
@@ -6,6 +6,9 @@
 // a locally meaningful name, when creating dart bindings
 
 import 'dart:math';
+
+import 'package:jnigen/src/logging/logging.dart';
+
 import 'package:jnigen/src/util/name_utils.dart';
 
 abstract class SymbolResolver {
@@ -29,8 +32,9 @@
 
   final Map<String, String> _importedNameToPackage = {};
   final Map<String, String> _packageToImportedName = {};
-  // return null if type's package cannot be resolved
-  // else return the fully qualified name of type
+
+  /// Returns the dart name of the [binaryName] in current translation context,
+  /// or `null` if the name cannot be resolved.
   @override
   String? resolve(String binaryName) {
     if (predefined.containsKey(binaryName)) {
@@ -57,6 +61,7 @@
     }
 
     final packageImport = getImport(package, binaryName);
+    log.finest('$package resolved to $packageImport for $binaryName');
     if (packageImport == null) {
       return null;
     }
@@ -67,6 +72,8 @@
           'qualified binaryName');
     }
 
+    // We always name imports with an underscore suffix, so that they can be
+    // never shadowed by a parameter or local variable.
     var importedName = '${pkgName}_';
     int suffix = 0;
     while (_importedNameToPackage.containsKey(importedName)) {
diff --git a/pkgs/jnigen/lib/src/config/config.dart b/pkgs/jnigen/lib/src/config/config.dart
index 4094e3c..136845f 100644
--- a/pkgs/jnigen/lib/src/config/config.dart
+++ b/pkgs/jnigen/lib/src/config/config.dart
@@ -9,6 +9,8 @@
 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.
@@ -142,6 +144,7 @@
     this.androidSdkConfig,
     this.mavenDownloads,
     this.summarizerOptions,
+    this.logLevel = Level.INFO,
     this.dumpJsonTo,
   });
 
@@ -205,8 +208,15 @@
   /// Additional options for the summarizer component
   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.
   String? dumpJsonTo;
 
+  static final _levels = Map.fromEntries(
+      Level.LEVELS.map((l) => MapEntry(l.name.toLowerCase(), l)));
   static Uri? _toDirUri(String? path) =>
       path != null ? Uri.directory(path) : null;
   static List<Uri>? _toUris(List<String>? paths) =>
@@ -231,7 +241,7 @@
       for (var exclusion in exclusions) {
         final split = exclusion.split('#');
         if (split.length != 2) {
-          throw FormatException('Error parsing exclusion: "$exclusion"; '
+          throw FormatException('Error parsing exclusion: "$exclusion": '
               'expected to be in binaryName#member format.');
         }
         filters.add(MemberNameFilter<T>.exclude(
@@ -252,6 +262,15 @@
       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)),
@@ -295,6 +314,12 @@
               androidExample: prov.getString(_Props.androidExample),
             )
           : null,
+      logLevel: logLevelFromString(
+        prov.getOneOf(
+          _Props.logLevel,
+          {'error', 'warning', 'info', 'debug', 'verbose'},
+        ),
+      ),
     );
     if (missingValues.isNotEmpty) {
       stderr.write('Following config values are required but not provided\n'
@@ -333,6 +358,7 @@
   static const dartRoot = 'dart_root';
   static const preamble = 'preamble';
   static const libraryName = 'library_name';
+  static const logLevel = 'log_level';
 
   static const mavenDownloads = 'maven_downloads';
   static const sourceDeps = '$mavenDownloads.source_deps';
diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart
index c6f410e..b6a64f5 100644
--- a/pkgs/jnigen/lib/src/generate_bindings.dart
+++ b/pkgs/jnigen/lib/src/generate_bindings.dart
@@ -10,8 +10,11 @@
 import 'config/config.dart';
 import 'tools/tools.dart';
 import 'writers/writers.dart';
+import 'logging/logging.dart';
 
 Future<void> generateJniBindings(Config config) async {
+  setLoggingLevel(config.logLevel);
+
   await buildSummarizerIfNotExists();
 
   final summarizer = SummarizerCommand(
@@ -72,7 +75,7 @@
   try {
     input = await summarizer.getInputStream();
   } on Exception catch (e) {
-    stderr.writeln('error obtaining API summary: $e');
+    log.fatal('Cannot obtain API summary: $e');
     return;
   }
   final stream = JsonDecoder().bind(Utf8Decoder().bind(input));
@@ -80,11 +83,11 @@
   try {
     json = await stream.single;
   } on Exception catch (e) {
-    stderr.writeln('error while parsing summary: $e');
+    log.fatal('Cannot parse summary: $e');
     return;
   }
   if (json == null) {
-    stderr.writeln('error: expected JSON element from summarizer.');
+    log.fatal('Expected JSON element from summarizer.');
     return;
   }
   final list = json as List;
@@ -93,6 +96,6 @@
     await outputWriter.writeBindings(list.map((c) => ClassDecl.fromJson(c)));
   } on Exception catch (e, trace) {
     stderr.writeln(trace);
-    stderr.writeln('error writing bindings: $e');
+    log.fatal('Error while writing bindings: $e');
   }
 }
diff --git a/pkgs/jnigen/lib/src/logging/logging.dart b/pkgs/jnigen/lib/src/logging/logging.dart
new file mode 100644
index 0000000..526981e
--- /dev/null
+++ b/pkgs/jnigen/lib/src/logging/logging.dart
@@ -0,0 +1,30 @@
+// 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:logging/logging.dart';
+
+const _ansiRed = '\x1b[31m';
+const _ansiDefault = '\x1b[39;49m';
+
+Logger log = Logger('jnigen');
+
+void setLoggingLevel(Level level) {
+  Logger.root.level = level;
+  Logger.root.onRecord.listen((r) {
+    var message = '(${r.loggerName}) ${r.level.name}: ${r.message}';
+    if (level == Level.SHOUT || level == Level.SEVERE) {
+      message = '$_ansiRed$message$_ansiDefault';
+    }
+    stderr.writeln(message);
+  });
+}
+
+extension FatalErrors on Logger {
+  void fatal(Object? message, {int exitCode = 2}) {
+    message = '${_ansiRed}Fatal: $message$_ansiDefault';
+    stderr.writeln(message);
+    exit(exitCode);
+  }
+}
diff --git a/pkgs/jnigen/lib/src/summary/summary.dart b/pkgs/jnigen/lib/src/summary/summary.dart
index 5a7d714..1282ee2 100644
--- a/pkgs/jnigen/lib/src/summary/summary.dart
+++ b/pkgs/jnigen/lib/src/summary/summary.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:io';
+import 'package:jnigen/src/logging/logging.dart';
 import 'package:jnigen/src/util/command_output.dart';
 
 /// A command based summary source which calls the ApiSummarizer command.
@@ -79,7 +80,7 @@
     args.addAll(extraArgs);
     args.addAll(classes);
 
-    stderr.writeln('[exec] $exec ${args.join(' ')}');
+    log.info('execute $exec ${args.join(' ')}');
     final proc = await Process.start(exec, args,
         workingDirectory: workingDirectory?.toFilePath() ?? '.');
     prefixedCommandOutputStream('[ApiSummarizer]', proc.stderr)
diff --git a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
index ed78566..b903bd6 100644
--- a/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
+++ b/pkgs/jnigen/lib/src/tools/android_sdk_tools.dart
@@ -5,6 +5,8 @@
 import 'dart:io';
 import 'package:path/path.dart';
 
+import 'package:jnigen/src/logging/logging.dart';
+
 class AndroidSdkTools {
   /// get path for android API sources
   static Future<String?> _getVersionDir(
@@ -25,7 +27,9 @@
 
   static Future<String?> getAndroidSourcesPath(
       {String? sdkRoot, required List<int> versionOrder}) async {
-    return _getVersionDir('sources', sdkRoot, versionOrder);
+    final dir = _getVersionDir('sources', sdkRoot, versionOrder);
+    log.info('Found sources at $dir');
+    return dir;
   }
 
   static Future<String?> _getFile(String relative, String file, String? sdkRoot,
@@ -34,6 +38,7 @@
     if (platform == null) return null;
     final filePath = join(platform, file);
     if (await File(filePath).exists()) {
+      log.info('Found $filePath');
       return filePath;
     }
     return null;
@@ -68,7 +73,7 @@
   /// 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<String> getGradleClasspaths([String androidProject = '.']) {
-    stderr.writeln('trying to obtain gradle classpaths...');
+    log.info('trying to obtain gradle classpaths...');
     final android = join(androidProject, 'android');
     final buildGradle = join(android, 'build.gradle');
     final buildGradleOld = join(android, 'build.gradle.old');
@@ -76,20 +81,23 @@
     final script = origBuild.readAsStringSync();
     origBuild.renameSync(buildGradleOld);
     origBuild.createSync();
+    log.finer('Writing temporary gradle script with stub function...');
     origBuild.writeAsStringSync('$script\n$_gradleListDepsFunction\n');
+    log.finer('Running gradle wrapper...');
     final procRes = Process.runSync('./gradlew', ['-q', 'listDependencies'],
         workingDirectory: android);
+    log.finer('Restoring build scripts');
     origBuild.writeAsStringSync(script);
     File(buildGradleOld).deleteSync();
     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` in $androidProject and try again\n');
+          '`flutter build apk`$inAndroidProject and try again\n');
     }
-    final gradleClassPaths = (procRes.stdout as String).split('\n');
-    if (gradleClassPaths.last.isEmpty) {
-      gradleClassPaths.removeLast();
-    }
-    return gradleClassPaths;
+    final classpaths = (procRes.stdout as String).trim().split('\n');
+    log.info('Found release build classpath with ${classpaths.length} entries');
+    return classpaths;
   }
 }
diff --git a/pkgs/jnigen/lib/src/tools/build_summarizer.dart b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
index b018327..d092549 100644
--- a/pkgs/jnigen/lib/src/tools/build_summarizer.dart
+++ b/pkgs/jnigen/lib/src/tools/build_summarizer.dart
@@ -7,6 +7,7 @@
 import 'package:path/path.dart';
 
 import 'package:jnigen/src/util/find_package.dart';
+import 'package:jnigen/src/logging/logging.dart';
 
 final toolPath = join('.', '.dart_tool', 'jnigen');
 final mvnTargetDir = join(toolPath, 'target');
@@ -16,23 +17,21 @@
 Future<void> buildApiSummarizer() async {
   final pkg = await findPackageRoot('jnigen');
   if (pkg == null) {
-    stderr.writeln('package jnigen not found!');
-    exitCode = 2;
+    log.fatal('package jnigen not found!');
     return;
   }
   final pom = pkg.resolve('java/pom.xml');
   await Directory(toolPath).create(recursive: true);
-  final mvnProc = await Process.start(
-      'mvn',
-      [
-        '--batch-mode',
-        '--update-snapshots',
-        '-f',
-        pom.toFilePath(),
-        'assembly:assembly'
-      ],
-      workingDirectory: toolPath,
-      mode: ProcessStartMode.inheritStdio);
+  final mvnArgs = [
+    '--batch-mode',
+    '--update-snapshots',
+    '-f',
+    pom.toFilePath(),
+    'assembly:assembly'
+  ];
+  log.info('execute mvn $mvnArgs');
+  final mvnProc = await Process.start('mvn', mvnArgs,
+      workingDirectory: toolPath, mode: ProcessStartMode.inheritStdio);
   await mvnProc.exitCode;
   File(targetJarFile).renameSync(jarFile);
   Directory(mvnTargetDir).deleteSync(recursive: true);
@@ -44,17 +43,17 @@
       await isPackageModifiedAfter(
           'jnigen', await File(jarFile).lastModified(), 'java/');
   if (isJarStale) {
-    stderr.writeln('Rebuilding ApiSummarizer component since sources '
+    log.info('Rebuilding ApiSummarizer component since sources '
         'have changed. This might take some time.');
   }
   if (!jarExists) {
-    stderr.write('Building ApiSummarizer component. '
+    log.info('Building ApiSummarizer component. '
         'This might take some time. \n'
         'The build will be cached for subsequent runs\n');
   }
   if (!jarExists || isJarStale || force) {
     await buildApiSummarizer();
   } else {
-    stderr.writeln('ApiSummarizer.jar exists. Skipping build..');
+    log.info('ApiSummarizer.jar exists. Skipping build..');
   }
 }
diff --git a/pkgs/jnigen/lib/src/tools/maven_tools.dart b/pkgs/jnigen/lib/src/tools/maven_tools.dart
index 52835d9..b121e39 100644
--- a/pkgs/jnigen/lib/src/tools/maven_tools.dart
+++ b/pkgs/jnigen/lib/src/tools/maven_tools.dart
@@ -4,6 +4,8 @@
 
 import 'dart:io';
 
+import 'package:jnigen/src/logging/logging.dart';
+
 /// This class provides some utility methods to download a sources / jars
 /// using maven along with transitive dependencies.
 class MavenTools {
@@ -11,19 +13,10 @@
   static const _tempClassPath = '__temp_classpath.xml';
   static const _tempTarget = '__mvn_target';
 
-  static bool _verbose = false;
-  static void setVerbose(bool enabled) => _verbose = enabled;
-
-  static void _verboseLog(Object? value) {
-    if (_verbose) {
-      stderr.writeln(value);
-    }
-  }
-
   /// Helper method since we can't pass inheritStdio option to [Process.run].
   static Future<int> _runCmd(String exec, List<String> args,
       [String? workingDirectory]) async {
-    _verboseLog('[exec] $exec ${args.join(" ")}');
+    log.info('execute $exec ${args.join(" ")}');
     final proc = await Process.start(exec, args,
         workingDirectory: workingDirectory,
         mode: ProcessStartMode.inheritStdio);
@@ -33,7 +26,7 @@
   static Future<void> _runMavenCommand(
       List<MavenDependency> deps, List<String> mvnArgs) async {
     final pom = _getStubPom(deps);
-    _verboseLog('using POM stub:\n$pom');
+    log.finer('using POM stub:\n$pom');
     await File(_tempPom).writeAsString(pom);
     await Directory(_tempTarget).create();
     await _runCmd('mvn', ['-f', _tempPom, ...mvnArgs]);
diff --git a/pkgs/jnigen/lib/src/writers/writers.dart b/pkgs/jnigen/lib/src/writers/writers.dart
index c1caeb7..c4f3843 100644
--- a/pkgs/jnigen/lib/src/writers/writers.dart
+++ b/pkgs/jnigen/lib/src/writers/writers.dart
@@ -8,6 +8,7 @@
 
 import 'package:jnigen/src/elements/elements.dart';
 import 'package:jnigen/src/config/config.dart';
+import 'package:jnigen/src/logging/logging.dart';
 import 'package:jnigen/src/util/find_package.dart';
 
 abstract class BindingsWriter {
@@ -58,7 +59,7 @@
     }
     final classNames = classesByName.keys.toSet();
 
-    stderr.writeln('Creating dart init file ...');
+    log.info('Creating dart init file ...');
     final initFileUri = dartRoot.resolve(_initFileName);
     final initFile = await File.fromUri(initFileUri).create(recursive: true);
     await initFile.writeAsString(DartPreludes.initFile(config.libraryName),
@@ -76,7 +77,7 @@
     for (var packageName in packages.keys) {
       final relativeFileName = '${packageName.replaceAll('.', '/')}.dart';
       final dartFileUri = dartRoot.resolve(relativeFileName);
-      stderr.writeln('Writing bindings for $packageName...');
+      log.info('Writing bindings for $packageName...');
       final dartFile = await File.fromUri(dartFileUri).create(recursive: true);
       final resolver = PackagePathResolver(
           config.importMap ?? const {}, packageName, classNames,
@@ -107,15 +108,17 @@
       await dartFileStream.close();
     }
     await cFileStream.close();
-    stderr.writeln('Running dart format...');
+    log.info('Running dart format...');
     final formatRes =
         await Process.run('dart', ['format', dartRoot.toFilePath()]);
-    if (formatRes.exitCode != 0) {
-      stderr.writeln('ERROR: dart format completed with '
-          'exit code ${formatRes.exitCode}');
+    // if negative exit code, likely due to an interrupt.
+    if (formatRes.exitCode > 0) {
+      log.fatal('Dart format completed with exit code ${formatRes.exitCode} '
+          'This usually means there\'s a syntax error in bindings.\n'
+          'Please look at the generated files and report a bug.');
     }
 
-    stderr.writeln('Copying auxiliary files...');
+    log.info('Copying auxiliary files...');
     await _copyFileFromPackage(
         'jni', 'src/dartjni.h', cRoot.resolve('$subdir/dartjni.h'));
     await _copyFileFromPackage(
@@ -125,7 +128,7 @@
           .replaceAll('{{LIBRARY_NAME}}', libraryName)
           .replaceAll('{{SUBDIR}}', subdir);
     });
-    stderr.writeln('Completed.');
+    log.info('Completed.');
   }
 
   Future<void> _copyFileFromPackage(String package, String relPath, Uri target,
@@ -140,7 +143,7 @@
       }
       await targetFile.writeAsString(source);
     } else {
-      stderr.writeln('package $package not found! '
+      log.warning('package $package not found! '
           'skipped copying ${target.toFilePath()}');
     }
   }
diff --git a/pkgs/jnigen/pubspec.yaml b/pkgs/jnigen/pubspec.yaml
index fd538fc..fe8defa 100644
--- a/pkgs/jnigen/pubspec.yaml
+++ b/pkgs/jnigen/pubspec.yaml
@@ -16,6 +16,7 @@
   path: ^1.8.0
   args: ^2.3.0
   yaml: ^3.1.0
+  logging: ^1.0.2
 
 dev_dependencies:
   lints: ^2.0.0