linkNativeAssetsHook function
- List<
ModuleInfo> moduleInfos, - String nitroNativePath, {
- 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;
}