linkPodspec function

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

Implementation

void linkPodspec(
  String pluginName,
  List<String> moduleLibs, {
  String baseDir = '.',
  List<ModuleInfo>? moduleInfos,
}) {
  final nitroNativePath = resolveNitroNativePath(baseDir);
  final podspecFile = File(p.join(baseDir, 'ios', '$pluginName.podspec'));
  if (!podspecFile.existsSync()) return;
  var content = podspecFile.readAsStringSync();
  bool modified = false;
  // Normalize source_files to 'Classes/**/*'.
  // Flutter's SPM-first template generates paths like '<plugin>/Sources/<plugin>/**/*'
  // which point to non-existent directories when CocoaPods is the build system,
  // causing "No files found matching ..." warnings and empty pod targets.
  final sourceFilesMatch = RegExp(r"s\.source_files\s*=\s*'([^']+)'").firstMatch(content);
  if (sourceFilesMatch != null && sourceFilesMatch.group(1) != 'Classes/**/*') {
    final badPath = sourceFilesMatch.group(1)!;
    // Only fix when the first segment is not 'Classes' and doesn't exist on disk.
    final firstSegment = badPath.split('/').first;
    final firstDir = Directory(p.join(podspecFile.parent.path, firstSegment));
    if (firstSegment != 'Classes' && !firstDir.existsSync()) {
      content = content.replaceFirst(
        sourceFilesMatch.group(0)!,
        "s.source_files = 'Classes/**/*'",
      );
      modified = true;
    }
  }
  if (!content.contains("s.swift_version = '5.9'")) {
    content = content.replaceFirst(
      RegExp(r"s\.swift_version\s*=\s*'.+?'"),
      "s.swift_version = '5.9'",
    );
    modified = true;
  }
  if (!content.contains("s.platform = :ios, '13.0'")) {
    content = content.replaceFirst(
      RegExp(r"s\.platform\s*=\s*:ios,\s*'.+?'"),
      "s.platform = :ios, '13.0'",
    );
    modified = true;
  }
  if (!content.contains('HEADER_SEARCH_PATHS')) {
    content = content.replaceFirst(
      's.pod_target_xcconfig = {',
      "s.pod_target_xcconfig = {\n    'HEADER_SEARCH_PATHS' => '\$(inherited) \"\${PODS_ROOT}/../.symlinks/plugins/nitro/src/native\" \"\${PODS_TARGET_SRCROOT}/../src\" \"\${PODS_TARGET_SRCROOT}/../lib/src/generated/cpp\"',",
    );
    modified = true;
  } else {
    // If it exists, ensure it has the src/ and generated/cpp/ paths.
    if (!content.contains('PODS_TARGET_SRCROOT}/../src') ||
        !content.contains('lib/src/generated/cpp')) {
      final match = RegExp(
        r"'HEADER_SEARCH_PATHS'\s*=>\s*'([^']+)'",
      ).firstMatch(content);
      if (match != null) {
        var paths = match.group(1)!;
        if (!paths.contains('PODS_TARGET_SRCROOT}/../src')) {
          paths += ' "\${PODS_TARGET_SRCROOT}/../src"';
        }
        if (!paths.contains('lib/src/generated/cpp')) {
          paths += ' "\${PODS_TARGET_SRCROOT}/../lib/src/generated/cpp"';
        }
        content = content.replaceFirst(
          match.group(0)!,
          "'HEADER_SEARCH_PATHS' => '$paths'",
        );
        modified = true;
      }
    }
  }
  if (!content.contains("'DEFINES_MODULE' => 'YES'")) {
    content = content.replaceFirst(
      's.pod_target_xcconfig = {',
      "s.pod_target_xcconfig = {\n    'DEFINES_MODULE' => 'YES',",
    );
    modified = true;
  }
  if (!content.contains("'CLANG_CXX_LANGUAGE_STANDARD'") &&
      !content.contains('c++17')) {
    content = content.replaceFirst(
      's.pod_target_xcconfig = {',
      "s.pod_target_xcconfig = {\n    'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',",
    );
    modified = true;
  }
  if (!content.contains("s.dependency 'nitro'")) {
    content = content.replaceFirst(
      's.pod_target_xcconfig = {',
      "s.dependency 'nitro'\n  s.pod_target_xcconfig = {",
    );
    modified = true;
  }
  // Sync generated Swift bridges into ios/Classes/ so Xcode can compile them
  // in the same module scope as the plugin's other Swift files.
  // Using a podspec source_files glob to ../lib/src/generated/swift/ does NOT
  // reliably work — types defined there are not always in scope for Classes/ files.
  if (modified) podspecFile.writeAsStringSync(content);
  createSharedHeaders(nitroNativePath, baseDir: baseDir);
  final classesDir = Directory(p.join(baseDir, 'ios', 'Classes'))
    ..createSync(recursive: true);
  File(
    p.join(classesDir.path, 'dart_api_dl.c'),
  ).writeAsStringSync(classesDartApiDlForwarder);
  syncBridgeFiles(baseDir);
  _copySwiftBridgesToClasses(classesDir, baseDir);
  // Remove the outer lib/src/generated/swift glob from the podspec if present,
  // since the bridge is now copied directly into Classes/ (avoids duplicate symbols).
  _removeSwiftGlobFromPodspec(podspecFile);

  // Link the main project source files.
  final cppInSrc = File(p.join(baseDir, 'src', '$pluginName.cpp'));
  if (cppInSrc.existsSync()) {
    cleanRedundantIncludes(cppInSrc);
    File(p.join(classesDir.path, '$pluginName.cpp')).writeAsStringSync(
      managedCppForwarder('../../src/$pluginName.cpp'),
    );
  }
  final cInSrc = File(p.join(baseDir, 'src', '$pluginName.c'));
  if (cInSrc.existsSync()) {
    cleanRedundantIncludes(cInSrc);
    File(
      p.join(classesDir.path, '$pluginName.c'),
    ).writeAsStringSync(classesCForwarder(pluginName));
  }

  // Link C++ module implementation files for iOS.
  // On Android each module is a separate .so via CMake. On iOS everything is
  // compiled into one pod binary, so only ios:NativeImpl.cpp modules need
  // a Hybrid*.cpp forwarder in ios/Classes/.
  // Windows-only or macos-only C++ modules must NOT get a forwarder here.
  if (moduleInfos != null) {
    // Discover specs for iOS-cpp filtering (per-platform, not broad Apple check).
    final libDir = Directory(p.join(baseDir, 'lib'));
    final specFiles = libDir.existsSync()
        ? libDir
              .listSync(recursive: true)
              .whereType<File>()
              .where((f) => f.path.endsWith('.native.dart'))
              .toList()
        : <File>[];
    final appleCppLibs = specFiles.where(isIosCppModule).map((f) {
      final stem = p
          .basename(f.path)
          .replaceAll(RegExp(r'\.native\.dart$'), '');
      return extractLibNameFromSpec(f) ?? stem;
    }).toSet();

    // Write forwarders only for Apple cpp modules.
    for (final m in moduleInfos.where((m) => m.isCpp)) {
      final className = _toPascalCase(m.lib);
      final forwarderFile = File(
        p.join(classesDir.path, 'Hybrid$className.cpp'),
      );
      if (appleCppLibs.contains(m.lib)) {
        // Apple C++ module — ensure forwarder is present/up-to-date.
        final implSrc = File(p.join(baseDir, 'src', 'Hybrid$className.cpp'));
        if (implSrc.existsSync()) {
          forwarderFile.writeAsStringSync(
            managedCppForwarder('../../src/Hybrid$className.cpp'),
          );
        }
      } else {
        // Non-Apple C++ module (e.g. Windows-only) — remove any stale forwarder.
        if (forwarderFile.existsSync()) forwarderFile.deleteSync();
      }
    }
  }

  ensureIosPackageSwift(pluginName, baseDir: baseDir, moduleInfos: moduleInfos);

  // Re-affirm the correct ../../src/ relative paths AFTER ensureIosPackageSwift,
  // which may write forwarders into Sources/NitroPubTestCpp/ with ../../../src/.
  // These are two different files, but belt-and-suspenders: always end with the
  // definitive Classes/ versions so a stale copy can never win.
  File(
    p.join(classesDir.path, 'dart_api_dl.c'),
  ).writeAsStringSync(classesDartApiDlForwarder);
  if (cppInSrc.existsSync()) {
    File(p.join(classesDir.path, '$pluginName.cpp')).writeAsStringSync(
      managedCppForwarder('../../src/$pluginName.cpp'),
    );
  }
  if (cInSrc.existsSync()) {
    File(
      p.join(classesDir.path, '$pluginName.c'),
    ).writeAsStringSync(classesCForwarder(pluginName));
  }
}