linkCMake function

void linkCMake(
  1. String pluginName,
  2. List<String> moduleLibs,
  3. String nitroNativePath, {
  4. String baseDir = '.',
  5. List<ModuleInfo>? moduleInfos,
})

Implementation

void linkCMake(
  String pluginName,
  List<String> moduleLibs,
  String nitroNativePath, {
  String baseDir = '.',
  List<ModuleInfo>? moduleInfos,
}) {
  createSharedHeaders(nitroNativePath, baseDir: baseDir);
  final cmakeFile = File(p.join(baseDir, 'src', 'CMakeLists.txt'));
  if (!cmakeFile.existsSync()) {
    generateCMake(
      pluginName,
      moduleLibs,
      nitroNativePath,
      baseDir: baseDir,
      moduleInfos: moduleInfos,
    );
    return;
  }
  var content = cmakeFile.readAsStringSync();
  bool modified = false;
  final stamp = _stampLinkSpecChecksum(content, computeLinkSpecChecksum(baseDir: baseDir));
  content = stamp.content;
  modified = modified || stamp.modified;
  const desiredNitroValue = _srcLocalNitroNativeCmakePath;
  final nitroNativeSetLine = 'set(NITRO_NATIVE "$desiredNitroValue")';
  if (!content.contains('NITRO_NATIVE')) {
    content = '$nitroNativeSetLine\n\n$content';
    modified = true;
  } else {
    final staleMatch = RegExp(
      r'set\(NITRO_NATIVE\s+"([^"]+)"\)',
    ).firstMatch(content);
    if (staleMatch != null && staleMatch.group(1) != desiredNitroValue) {
      content = content.replaceFirst(staleMatch.group(0)!, nitroNativeSetLine);
      modified = true;
    }
  }
  if (!content.contains('CMAKE_CXX_STANDARD')) {
    // Inject C++17 standard after the project() declaration.
    content = content.replaceFirstMapped(
      RegExp(r'project\([^)]+\)\s*\n'),
      (m) => '${m.group(0)!}\nset(CMAKE_CXX_STANDARD ${BuildVersions.cmakeCxxStandard})\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n',
    );
    modified = true;
  }
  if (!content.contains(r'${NITRO_NATIVE}')) {
    content = content.replaceFirst(
      'target_include_directories($pluginName PRIVATE',
      'target_include_directories($pluginName PRIVATE\n  "\${NITRO_NATIVE}"',
    );
    modified = true;
  }
  if (!content.contains('dart_api_dl.c')) {
    content = content.replaceFirst(
      'add_library($pluginName SHARED',
      'add_library($pluginName SHARED\n  "dart_api_dl.c"',
    );
    modified = true;
  }
  final bridgeRel = '../lib/src/generated/cpp/$pluginName.bridge.g.cpp';
  if (!content.contains(bridgeRel)) {
    content = content.replaceFirst(
      'add_library($pluginName SHARED',
      'add_library($pluginName SHARED\n  "\${CMAKE_CURRENT_SOURCE_DIR}/$bridgeRel"',
    );
    modified = true;
  }

  // Add the main plugin's HybridXxx.cpp impl file when:
  //   • the module uses NativeImpl.cpp on android/linux (isNativeCpp) — the
  //     src/ CMakeLists is for Android/Linux only; macOS/iOS are handled by SPM/CocoaPods.
  //   • the file exists in src/, and
  //   • it is not already listed in the cmake (either inline or in a NOT ANDROID guard).
  //
  // When android uses Kotlin (isAndroidCpp=false) but linux uses C++, wrap in
  // `if(NOT ANDROID)` so the NDK build skips the C++ impl stub.
  if (moduleInfos != null) {
    final mainInfo = moduleInfos.firstWhere(
      (m) => m.lib == pluginName,
      orElse: () => ModuleInfo(lib: pluginName, module: pluginName, isCpp: false),
    );
    if (mainInfo.isNativeCpp) {
      final className = _toPascalCase(
        mainInfo.module.isNotEmpty ? mainInfo.module : pluginName,
      );
      final implName = 'Hybrid$className.cpp';
      final implFile = File(p.join(baseDir, 'src', implName));
      if (implFile.existsSync() && !content.contains('"$implName"')) {
        if (mainInfo.isAndroidCpp) {
          // Android uses C++ directly — embed impl in add_library.
          content = content.replaceFirst(
            'add_library($pluginName SHARED',
            'add_library($pluginName SHARED\n  "$implName"',
          );
        } else {
          // Linux-only C++ — exclude from Android NDK builds.
          content = content.replaceFirst(
            'target_include_directories($pluginName PRIVATE',
            'if(NOT ANDROID)\n  target_sources($pluginName PRIVATE "$implName")\nendif()\ntarget_include_directories($pluginName PRIVATE',
          );
        }
        modified = true;
      }
    }
  }

  for (final lib in moduleLibs) {
    if (lib != pluginName && !content.contains('add_library($lib ')) {
      final info = moduleInfos?.firstWhere(
        (m) => m.lib == lib,
        orElse: () => ModuleInfo(lib: lib, module: lib, isCpp: false),
      );
      // Use isNativeCpp (android/linux) — only those platforms put
      // HybridXxx.cpp into src/CMakeLists.txt. Windows-only cpp uses
      // windows/CMakeLists.txt instead.
      content += ct.cmakeModuleTarget(
        lib,
        isCpp: info?.isNativeCpp ?? false,
        isAndroidCpp: info?.isAndroidCpp ?? false,
      );
      modified = true;
    }
  }

  // Retrofit the NITRO_IMPL_SRC guard onto a pre-separation src/CMakeLists
  // once a module opts into per-platform desktop impls (issue #12): the
  // platform CMakeLists set NITRO_IMPL_SRC_<lib>, but an existing file that
  // hardcodes `target_sources(<lib> PRIVATE "HybridXxx.cpp")` (or compiles
  // the impl inline in add_library) silently ignores them — the build keeps
  // compiling the shared impl while the per-platform files sit unused.
  // Only fires for modules where separation is actually requested/active,
  // so never-opted-in projects keep byte-identical CMakeLists. The guard's
  // else-branch preserves the old behavior exactly when the variable is
  // unset (e.g. Android builds of the same file).
  if (moduleInfos != null) {
    for (final m in moduleInfos.where((m) => m.isCpp)) {
      final varName = ct.nitroImplSrcVar(m.lib);
      if (content.contains(varName)) continue; // already guarded
      final cls = _toPascalCase(m.lib);
      final separationActive =
          (m.windowsIsCpp && (m.windowsRequestsSeparateImpl || hasCustomPlatformImpl(baseDir, 'windows', cls))) ||
          (m.linuxIsCpp && (m.linuxRequestsSeparateImpl || hasCustomPlatformImpl(baseDir, 'linux', cls)));
      if (!separationActive) continue;
      // Both className conventions appear in the wild (older links derived it
      // from `module`, stubs derive it from `lib`).
      for (final className in {cls, _toPascalCase(m.module)}) {
        final implName = 'Hybrid$className.cpp';
        final wrapped = RegExp(
          'if\\(NOT ANDROID\\)\\s*\\n\\s*target_sources\\(${RegExp.escape(m.lib)} PRIVATE "${RegExp.escape(implName)}"\\)\\s*\\nendif\\(\\)\\n?',
        );
        final bare = RegExp(
          'target_sources\\(${RegExp.escape(m.lib)} PRIVATE "${RegExp.escape(implName)}"\\)\\n?',
        );
        final inline = '\n  "$implName"';
        if (wrapped.hasMatch(content)) {
          content = content.replaceFirst(wrapped, ct.implSourcesBlock(m.lib, className, unguarded: false));
          modified = true;
          break;
        } else if (bare.hasMatch(content)) {
          content = content.replaceFirst(bare, ct.implSourcesBlock(m.lib, className, unguarded: m.isAndroidCpp));
          modified = true;
          break;
        } else if (content.contains('add_library(${m.lib} SHARED') && content.contains(inline)) {
          // Impl listed inline inside add_library (android-cpp layout):
          // move it out into the guarded block, same unconditional semantics.
          content = content.replaceFirst(inline, '');
          final addLib = RegExp('add_library\\(${RegExp.escape(m.lib)} SHARED[^)]*\\)\\n');
          final match = addLib.firstMatch(content);
          if (match != null) {
            content = content.replaceFirst(match.group(0)!, '${match.group(0)!}${ct.implSourcesBlock(m.lib, className, unguarded: true)}');
          } else {
            content += ct.implSourcesBlock(m.lib, className, unguarded: true);
          }
          modified = true;
          break;
        }
      }
    }
  }
  if (modified) cmakeFile.writeAsStringSync(content);
}