linkNativeAssetsHook function

bool linkNativeAssetsHook(
  1. List<ModuleInfo> moduleInfos,
  2. String nitroNativePath, {
  3. String baseDir = '.',
})

Writes hook/build.dart, vendors nitro's C sources/headers into src/nitro_native/, and injects the hook deps into pubspec.yaml for the all-C++ modules in moduleInfos.

Returns true only if it actually wrote the hook. It REFUSES (returns false, touches nothing) when the native-assets path is unsafe/inapplicable, so a plugin with its own native build (e.g. nitro_server's cmake engine) is never clobbered and the caller can fall back to the standard CMake/podspec link: • an existing hand-written hook/build.dart (not nitrogen-generated), or • any generated *.g.dart still on the DynamicLibrary (_dylib) path, i.e. a module fell back (TypedData / @zeroCopy / Stream-of-imported- struct / mixed Swift-Kotlin) so a CBuilder hook would mismatch it.

Implementation

bool linkNativeAssetsHook(
  List<ModuleInfo> moduleInfos,
  String nitroNativePath, {
  String baseDir = '.',
}) {
  final cppModules = moduleInfos.where((m) => m.isCpp).toList();
  if (cppModules.isEmpty) return false;

  // ── Safety gate (a): never overwrite the plugin's own native build ────────
  final hookFile = File(p.join(baseDir, 'hook', 'build.dart'));
  if (hookFile.existsSync() && !hookFile.readAsStringSync().contains('GENERATED BY nitrogen')) {
    stdout.writeln(
      "  native-assets: SKIPPED — hook/build.dart is hand-written (not nitrogen-generated), "
      "so this plugin has its own native build; refusing to overwrite it. "
      "Delete the hook first if you really want the generated native-assets hook.",
    );
    return false;
  }

  // ── Safety gate (b): every module must actually be on the @Native path ────
  // The emit is opt-in per the native_assets build option; an ineligible module
  // (TypedData/@zeroCopy/Stream-of-imported-struct, or a Swift/Kotlin module)
  // falls back to the _dylib path, which a CBuilder hook can't satisfy.
  final fellBack = <String>[];
  final libDir = Directory(p.join(baseDir, 'lib'));
  if (libDir.existsSync()) {
    for (final f in libDir.listSync(recursive: true).whereType<File>()) {
      if (f.path.endsWith('.g.dart') && f.readAsStringSync().contains('_dylib')) {
        fellBack.add(p.relative(f.path, from: baseDir));
      }
    }
  }
  if (fellBack.isNotEmpty) {
    stdout.writeln(
      "  native-assets: SKIPPED — not all modules use the @Native path (${fellBack.join(', ')} "
      "still resolve via DynamicLibrary: likely TypedData/@zeroCopy/Stream or a Swift/Kotlin "
      "module). The native-assets hook only supports fully-eligible, self-contained all-C++ "
      "modules; leaving the standard CMake/podspec build in place.",
    );
    return false;
  }

  // ── External-build mode: the plugin has its own cmake project (src/
  // CMakeLists.txt — an engine + external deps). Drive THAT build and declare
  // the produced library as the code asset, instead of CBuilder compiling a
  // fixed source list. The generated Dart is already @Native (gates passed);
  // @Native is build-agnostic (binds by assetId + symbols), so it resolves the
  // cmake-built library the same as a CBuilder one.
  if (File(p.join(baseDir, 'src', 'CMakeLists.txt')).existsSync()) {
    if (cppModules.length != 1) {
      stdout.writeln(
        "  native-assets: SKIPPED — external cmake build currently supports a single C++ "
        "module (found ${cppModules.map((m) => m.lib).join(', ')}); using standard linking.",
      );
      return false;
    }
    final mod = cppModules.single;
    final libStem = mod.lib.replaceAll('-', '_');
    final pkg = _readPubspecName(baseDir) ?? libStem;
    Directory(p.join(baseDir, 'hook')).createSync(recursive: true);
    File(p.join(baseDir, 'hook', 'build.dart'))
        .writeAsStringSync(buildExternalCmakeHookDart(libStem: libStem, pkg: pkg));
    // The external cmake hook imports ONLY hooks + code_assets (it drives cmake
    // via Process.run — no CBuilder). Do NOT inject native_toolchain_c: it
    // pins hooks ^2.x and would conflict with a plugin on hooks ^1.x.
    injectNativeAssetsPubspecDeps(
      p.join(baseDir, 'pubspec.yaml'),
      deps: {'hooks': nativeAssetsHookDeps['hooks']!, 'code_assets': nativeAssetsHookDeps['code_assets']!},
    );
    stdout.writeln('  native-assets: wrote hook/build.dart (external cmake build → code asset: $libStem)');
    _warnDoubleBundle([libStem], baseDir);
    return true;
  }

  // 1. Vendor nitro's C sources + headers next to the hook so CBuilder finds
  //    them package-relative (mirrors how linkCMake copies nitro.h into include
  //    dirs). Portable: no machine-specific absolute path baked into the hook.
  //    Shared by every module's asset.
  final vendorDir = Directory(p.join(baseDir, 'src', 'nitro_native'))..createSync(recursive: true);
  final nitroNative = Directory(nitroNativePath);
  for (final name in [...nativeAssetsVendoredCSources, ...nativeAssetsVendoredHeaders]) {
    final src = File(p.join(nitroNative.path, name));
    if (src.existsSync()) src.copySync(p.join(vendorDir.path, name));
  }
  final internalSrc = Directory(p.join(nitroNative.path, 'internal'));
  if (internalSrc.existsSync()) {
    final internalDst = Directory(p.join(vendorDir.path, 'internal'))..createSync(recursive: true);
    for (final f in internalSrc.listSync().whereType<File>()) {
      f.copySync(p.join(internalDst.path, p.basename(f.path)));
    }
  }

  // 2. One code asset per C++ module. The impl path is included UNCONDITIONALLY
  //    (the user's impl, same convention as CMake's target_sources) — it may not
  //    exist yet at link time (chicken-and-egg: hook is written during generate,
  //    impl written after). The generated <lib>.impl.g.cpp starter guides the
  //    user; the build errors clearly if src/HybridXxx.cpp is absent.
  final modules = [
    for (final mod in cppModules)
      (
        libStem: mod.lib.replaceAll('-', '_'),
        cppSources: <String>[
          'lib/src/generated/cpp/${mod.lib}.bridge.g.cpp',
          // Impl filename follows the lib stem (Hybrid<PascalCase(lib)>.cpp) —
          // the SAME convention as linkCMake (cmake.dart: className from m.lib),
          // NOT the module class name (e.g. lib `nitro_server` -> module class
          // `NitroServerNative` but impl file `HybridNitroServer.cpp`).
          'src/Hybrid${_toPascalCase(mod.lib)}.cpp',
        ],
      ),
  ];

  // 3. hook/build.dart (one CBuilder.library per module)
  Directory(p.join(baseDir, 'hook')).createSync(recursive: true);
  File(p.join(baseDir, 'hook', 'build.dart')).writeAsStringSync(buildHookDart(modules: modules));

  // 4. pubspec hook deps (regular dependencies — consumers run the hook).
  injectNativeAssetsPubspecDeps(p.join(baseDir, 'pubspec.yaml'));

  stdout.writeln('  native-assets: wrote hook/build.dart + vendored nitro C runtime '
      '(asset${modules.length == 1 ? '' : 's'}: ${modules.map((m) => m.libStem).join(', ')})');
  _warnDoubleBundle([for (final m in modules) m.libStem], baseDir);
  return true;
}