buildExternalCmakeHookDart function
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;
}
''';
}