linkBuildYamlSourcesExcludes function

void linkBuildYamlSourcesExcludes({
  1. String baseDir = '.',
})

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. Ensures the plugin's build.yaml carries sources excludes that keep build_runner's file-discovery walk out of the example app's platform build output (issue #20).

Once example/ has been built, example/{ios,macos}/.symlinks/plugins/<name> symlinks straight back to the plugin root; build_runner follows symlinks with no cycle detection and hangs forever with no output. nitrogen generate deletes those dirs before every run — but a plain dart run build_runner build/watch has no such guard. With the excludes in place the walk never enters example/ at all, making direct build_runner invocations safe too.

Behavior: creates build.yaml from the template when absent; inserts the sources: block under $default: when the file exists without one; NEVER touches a file that already declares sources: (the user owns their customization — doctor reports if it looks insufficient).

Implementation

void linkBuildYamlSourcesExcludes({String baseDir = '.'}) {
  final file = File(p.join(baseDir, 'build.yaml'));
  if (!file.existsSync()) {
    file.writeAsStringSync(sft.buildYamlTemplate());
    stdout.writeln('  build.yaml: created with sources excludes (guards direct build_runner runs against the example symlink-cycle hang)');
    return;
  }
  final content = file.readAsStringSync();
  if (content.contains('sources:')) return; // user-owned customization
  const sourcesBlock =
      '    sources:\n'
      '      include:\n'
      '        - lib/**\n'
      '        - \$package\$\n'
      '        - pubspec.yaml\n'
      '      exclude:\n'
      '        - example/**\n'
      '        - "**/.symlinks/**"\n'
      '        - "**/ephemeral/**"\n';
  // Insert directly under the `$default:` target — the shape both the
  // nitrogen template and flutter-created plugins use.
  final anchor = RegExp(r'^(\s*)\$default:\s*$', multiLine: true).firstMatch(content);
  if (anchor == null) {
    stdout.writeln('  build.yaml: unrecognized shape — add sources excludes for example/**, **/.symlinks/**, **/ephemeral/** yourself (see nitrogen doctor)');
    return;
  }
  final insertAt = content.indexOf('\n', anchor.end) + 1;
  final updated = content.substring(0, insertAt) + sourcesBlock + content.substring(insertAt);
  file.writeAsStringSync(updated);
  stdout.writeln('  build.yaml: added sources excludes (guards direct build_runner runs against the example symlink-cycle hang)');
}