linkDesktopPubspecFfiOnly function
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")');
}
}