linkDesktopPubspecFfiOnly 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.
Removes pluginClass: from the plugin pubspec's windows:/linux:
platform entries when that platform is a Nitro C++ (pure-FFI) backend.
Declaring BOTH pluginClass and ffiPlugin: true for a desktop platform
makes Flutter's tooling classify the plugin as method-channel: the app's
generated_plugins.cmake then links a <plugin>_plugin CMake target that
a pure-FFI plugin never defines —
CMake Error: No target <plugin>_plugin
— blocking every Windows/Linux build of the consuming app (issue #10).
The ffiPlugin-only form routes through FLUTTER_FFI_PLUGIN_LIST and the
plugin's <plugin>_bundled_libraries instead, which is what the
generated desktop CMakeLists provide.
Handles both YAML styles (block children and inline { ... } flow maps),
is idempotent, and touches nothing outside the two desktop entries —
android/ios/macos keep their pluginClass (those platforms genuinely
register a plugin class).
Implementation
void linkDesktopPubspecFfiOnly(
List<ModuleInfo> moduleInfos, {
String baseDir = '.',
}) {
final pubspecFile = File(p.join(baseDir, 'pubspec.yaml'));
if (!pubspecFile.existsSync()) return;
final fixWindows = moduleInfos.any((m) => m.windowsIsCpp);
final fixLinux = moduleInfos.any((m) => m.linuxIsCpp);
if (!fixWindows && !fixLinux) return;
final targets = <String>{if (fixWindows) 'windows', if (fixLinux) 'linux'};
final lines = pubspecFile.readAsStringSync().split('\n');
final out = <String>[];
var inPlatforms = false;
var platformsIndent = -1;
var changed = false;
int indentOf(String l) => l.length - l.trimLeft().length;
for (var i = 0; i < lines.length; i++) {
final line = lines[i];
final trimmed = line.trim();
if (trimmed == 'platforms:') {
inPlatforms = true;
platformsIndent = indentOf(line);
out.add(line);
continue;
}
if (inPlatforms && trimmed.isNotEmpty && indentOf(line) <= platformsIndent) {
inPlatforms = false; // left the platforms block
}
final flowMatch = inPlatforms ? RegExp(r'^(\s*)(\w+):\s*\{(.*)\}\s*$').firstMatch(line) : null;
if (flowMatch != null && targets.contains(flowMatch.group(2))) {
// Inline flow map: windows: { pluginClass: Xxx, ffiPlugin: true }
final entries = flowMatch.group(3)!.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
if (entries.any((e) => e.startsWith('ffiPlugin:')) && entries.any((e) => e.startsWith('pluginClass:'))) {
entries.removeWhere((e) => e.startsWith('pluginClass:'));
out.add('${flowMatch.group(1)}${flowMatch.group(2)}: { ${entries.join(', ')} }');
changed = true;
continue;
}
}
final blockKey = inPlatforms ? RegExp(r'^(\s*)(\w+):\s*$').firstMatch(line) : null;
if (blockKey != null && targets.contains(blockKey.group(2))) {
// Block form: collect the child lines (strictly deeper indent).
final keyIndent = indentOf(line);
var end = i + 1;
while (end < lines.length && (lines[end].trim().isEmpty || indentOf(lines[end]) > keyIndent)) {
end++;
}
final children = lines.sublist(i + 1, end);
final hasFfi = children.any((l) => l.trim().startsWith('ffiPlugin:') && l.contains('true'));
final hasClass = children.any((l) => l.trim().startsWith('pluginClass:'));
out.add(line);
for (final child in children) {
if (hasFfi && hasClass && child.trim().startsWith('pluginClass:')) {
changed = true;
continue;
}
out.add(child);
}
i = end - 1;
continue;
}
out.add(line);
}
if (changed) {
pubspecFile.writeAsStringSync(out.join('\n'));
stdout.writeln(' pubspec.yaml: removed pluginClass from FFI-only desktop platform entries (fixes "No target <plugin>_plugin")');
}
}