buildExternalCmakeHookDart function

String buildExternalCmakeHookDart({
  1. required String libStem,
  2. required String pkg,
  3. String cmakeSrcDir = 'src',
})

hook/build.dart for a plugin with its own cmake project (cmakeSrcDir): drives that build and declares the produced library as the [libStem] code asset. The CodeAsset name is the OS-independent lib stem (matching @Native(assetId: 'package:[pkg]/[libStem]')), and the cmake target's output name must be libStem. Cross-compiles via cmake's own toolchain vars derived from the build config (NDK toolchain file / CMAKE_SYSTEM_NAME=iOS / OSX arch); external deps are the plugin cmake's job. NITRO_CMAKE_ARGS (a ;-delimited env list) forwards extra configure args for prebuilt dep roots.

Implementation

String buildExternalCmakeHookDart({
  required String libStem,
  required String pkg,
  String cmakeSrcDir = 'src',
}) {
  return '''
// GENERATED BY nitrogen — native-assets build hook (EXTERNAL cmake build).
import 'dart:io';

import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';

// Drives the plugin's own cmake project ($cmakeSrcDir/CMakeLists.txt) so an
// external C++ engine + deps link normally, then declares the built library as
// the `$libStem` code asset (name = OS-independent lib stem, matching
// @Native(assetId: 'package:$pkg/$libStem')).
void main(List<String> args) async {
  await build(args, (input, output) async {
    if (!input.config.buildCodeAssets) return;
    final code = input.config.code;
    final os = code.targetOS;
    final buildDir = Directory.fromUri(input.outputDirectory.resolve('cmake'))..createSync(recursive: true);
    final srcDir = input.packageRoot.resolve('$cmakeSrcDir').toFilePath();

    Future<void> cmake(List<String> a) async {
      final r = await Process.run('cmake', a, workingDirectory: buildDir.path);
      if (r.exitCode != 0) {
        throw Exception('cmake \${a.join(' ')} failed (\${r.exitCode}):\\n\${r.stdout}\\n\${r.stderr}');
      }
    }

    // Per-target cross-compile flags from the build config (external deps are
    // the plugin cmake's job — this only supplies the toolchain).
    final cross = <String>[];
    switch (os) {
      case OS.android:
        final tc = _androidToolchainFile(code.cCompiler?.compiler);
        if (tc == null) {
          throw Exception(
            'native-assets: Android cross-compile needs the NDK. Set ANDROID_NDK_HOME '
            '(or ANDROID_NDK / ANDROID_NDK_ROOT), or build via `flutter build` which '
            'provides the NDK compiler in the build config.',
          );
        }
        final abi = switch (code.targetArchitecture) {
          Architecture.arm64 => 'arm64-v8a',
          Architecture.arm => 'armeabi-v7a',
          Architecture.x64 => 'x86_64',
          Architecture.ia32 => 'x86',
          final a => throw Exception('native-assets: unsupported Android arch \$a'),
        };
        cross.addAll([
          '-DCMAKE_TOOLCHAIN_FILE=\${tc.toFilePath()}',
          '-DANDROID_ABI=\$abi',
          '-DANDROID_PLATFORM=android-\${code.android.targetNdkApi}',
        ]);
      case OS.iOS:
        final arch = switch (code.targetArchitecture) {
          Architecture.arm64 => 'arm64',
          Architecture.x64 => 'x86_64',
          final a => throw Exception('native-assets: unsupported iOS arch \$a'),
        };
        cross.addAll([
          '-DCMAKE_SYSTEM_NAME=iOS',
          '-DCMAKE_OSX_SYSROOT=\${code.iOS.targetSdk == IOSSdk.iPhoneSimulator ? 'iphonesimulator' : 'iphoneos'}',
          '-DCMAKE_OSX_ARCHITECTURES=\$arch',
          '-DCMAKE_OSX_DEPLOYMENT_TARGET=\${code.iOS.targetVersion}',
        ]);
      case OS.macOS:
        final arch = switch (code.targetArchitecture) {
          Architecture.arm64 => 'arm64',
          Architecture.x64 => 'x86_64',
          final a => throw Exception('native-assets: unsupported macOS arch \$a'),
        };
        cross.addAll([
          '-DCMAKE_OSX_ARCHITECTURES=\$arch',
          '-DCMAKE_OSX_DEPLOYMENT_TARGET=\${code.macOS.targetVersion}',
        ]);
      default:
        break; // Linux / Windows: native host build.
    }

    // NITRO_CMAKE_ARGS: extra configure args (`;`-delimited, so spaces survive)
    // for prebuilt dep roots. Spliced BEFORE `cross` so a user arg can't clobber
    // the generated toolchain/arch (cmake is last-wins on cache vars).
    final extra = (Platform.environment['NITRO_CMAKE_ARGS'] ?? '')
        .split(';')
        .where((s) => s.isNotEmpty)
        .toList();
    if (extra.isNotEmpty) stderr.writeln('native-assets: appending cmake args from NITRO_CMAKE_ARGS: \$extra');
    await cmake(['-S', srcDir, '-B', buildDir.path, '-DCMAKE_BUILD_TYPE=Release', ...extra, ...cross]);
    await cmake(['--build', buildDir.path, '--config', 'Release', '--parallel']);

    // Locate the produced shared library (cmake target output name == $libStem).
    final libFile = switch (os) {
      OS.windows => '$libStem.dll',
      OS.macOS || OS.iOS => 'lib$libStem.dylib',
      _ => 'lib$libStem.so',
    };
    final hits = buildDir
        .listSync(recursive: true)
        .whereType<File>()
        .where((f) => f.uri.pathSegments.last == libFile)
        .toList();
    if (hits.isEmpty) {
      throw Exception('native-assets: cmake produced no \$libFile under \${buildDir.path} '
          '(ensure the cmake target OUTPUT_NAME is "$libStem")');
    }

    output.assets.code.add(CodeAsset(
      package: '$pkg',
      name: '$libStem', // OS-independent — matches @Native assetId (Bug C)
      linkMode: DynamicLoadingBundled(),
      file: hits.first.uri,
    ));
    // Re-run this hook (→ cmake rebuild) whenever the generated bridge or any
    // src/ source changes — otherwise a regenerated bridge (new checksum) keeps
    // the cached dylib and the Dart<->native checksum guard fails at runtime.
    for (final dir in ['lib/src/generated/cpp', '$cmakeSrcDir']) {
      final d = Directory.fromUri(input.packageRoot.resolve('\$dir/'));
      if (d.existsSync()) {
        for (final f in d.listSync(recursive: true).whereType<File>()) {
          output.dependencies.add(f.uri);
        }
      }
    }
  });
}

// Locates the NDK's cmake toolchain file: first by walking up from the
// native-assets compiler path (.../toolchains/llvm/prebuilt/<host>/bin/clang →
// NDK root), else from the ANDROID_NDK* env vars. Null if the NDK isn't found.
Uri? _androidToolchainFile(Uri? compiler) {
  Uri? tcIn(Uri ndk) {
    final tc = ndk.resolve('build/cmake/android.toolchain.cmake');
    return File.fromUri(tc).existsSync() ? tc : null;
  }

  if (compiler != null) {
    var dir = compiler.resolve('.');
    for (var i = 0; i < 8; i++) {
      final tc = tcIn(dir);
      if (tc != null) return tc;
      final parent = dir.resolve('../');
      if (parent == dir) break;
      dir = parent;
    }
  }
  for (final v in const ['ANDROID_NDK_HOME', 'ANDROID_NDK', 'ANDROID_NDK_ROOT', 'ANDROID_NDK_LATEST_HOME']) {
    final e = Platform.environment[v];
    if (e != null && e.isNotEmpty) {
      final tc = tcIn(Directory(e).uri);
      if (tc != null) return tc;
    }
  }
  return null;
}
''';
}