linkAndroidConsumerRules function
Writes/patches android/consumer-rules.pro with the R8/ProGuard keep
rules every Kotlin-on-Android Nitro module needs.
The generated JNI bridge (nitro.<lib>_module.<Module>JniBridge) and the
hand-written impl (under the plugin's Android namespace) are reached
from C++ via FindClass + GetStaticMethodID by exact name AND
signature — R8 has no static-analysis visibility into that, so without
these rules it may rename, remove, or (in "full mode") mis-optimize them.
Uses includedescriptorclasses — not a stronger form of a plain -keep,
a DIFFERENT protection: plain -keep class X { *; } only protects a
member from removal/renaming, not the parameter/return TYPES referenced
in its signature. For methods JNI calls by exact signature (every
_call trampoline, several of which take many long/data-class params
for suspend methods), an altered parameter type produces a VerifyError
at the exact call site — a real, previously-hit crash
("VerifyError ... Long (Low Half)"), not a hypothetical one. This is
ProGuard's own documented remedy for native/JNI-called methods.
Idempotent and additive: only touches the marked block (creating it if
absent), never the rest of the file, so a plugin author's own rules for
unrelated dependencies survive every nitrogen link run. Re-derives the
block's content from the CURRENT module list each time, so e.g. adding a
second Nitro module to the plugin updates the keep rules automatically.
Implementation
void linkAndroidConsumerRules(
List<Map<String, String>> kotlinModules, {
String baseDir = '.',
}) {
if (kotlinModules.isEmpty) return;
final androidDir = Directory(p.join(baseDir, 'android'));
if (!androidDir.existsSync()) return;
String? namespace;
for (final candidate in [
File(p.join(baseDir, 'android', 'build.gradle')),
File(p.join(baseDir, 'android', 'build.gradle.kts')),
]) {
if (!candidate.existsSync()) continue;
final m = RegExp(r'''namespace\s*=?\s*["']([^"']+)["']''').firstMatch(candidate.readAsStringSync());
if (m != null) namespace = m.group(1);
break;
}
final bridgePackages = kotlinModules.map((m) => 'nitro.${(m['lib'] ?? '').replaceAll('-', '_')}_module').toSet().toList()..sort();
final block = StringBuffer()
..writeln(_nitroConsumerRulesBeginMarker)
..writeln('# The generated Nitro JNI bridge and its hand-written impl are reached')
..writeln('# from C++ via FindClass + GetStaticMethodID by exact name and signature.')
..writeln('# includedescriptorclasses additionally keeps the parameter/return types')
..writeln('# referenced in those signatures — without it R8 full mode can still')
..writeln('# rename/merge them, producing a VerifyError at the JNI call site even')
..writeln('# though the method itself is "kept". See linkAndroidConsumerRules in')
..writeln('# nitrogen_cli for the full explanation.');
for (final pkg in bridgePackages) {
block.writeln('-keep,includedescriptorclasses class $pkg.** {');
block.writeln(' *;');
block.writeln('}');
}
if (namespace != null) {
block.writeln('-keep,includedescriptorclasses class $namespace.** {');
block.writeln(' *;');
block.writeln('}');
}
block
..writeln()
..writeln('# JNI resolves native methods (Kotlin `external fun`) by exact name AND')
..writeln('# signature — includedescriptorclasses protects their parameter/return')
..writeln('# types too, not just their names.')
..writeln('-keepclasseswithmembernames,includedescriptorclasses class * {')
..writeln(' native <methods>;')
..writeln('}')
..write(_nitroConsumerRulesEndMarker);
final rulesFile = File(p.join(androidDir.path, 'consumer-rules.pro'));
final existing = rulesFile.existsSync() ? rulesFile.readAsStringSync() : '';
final markerPattern = RegExp(
'${RegExp.escape(_nitroConsumerRulesBeginMarker)}.*?${RegExp.escape(_nitroConsumerRulesEndMarker)}',
dotAll: true,
);
final String updated;
if (markerPattern.hasMatch(existing)) {
updated = existing.replaceFirst(markerPattern, block.toString());
} else if (existing.trim().isEmpty) {
updated = '$block\n';
} else {
updated = '${existing.trimRight()}\n\n$block\n';
}
if (updated != existing) rulesFile.writeAsStringSync(updated);
}