execute method
Executes the generation logic and returns the exit code. Does NOT call exit().
Implementation
Future<int> execute() async {
final projectDir = findNitroProjectRoot();
if (projectDir == null) {
stderr.writeln(red('❌ No Nitro project found in . or its subdirectories (must have nitro dependency in pubspec.yaml).'));
return 1;
}
// If we're not in the project root, let the user know we've found it
if (projectDir.path != Directory.current.path) {
stdout.writeln(gray(' 📂 Found project in: ${projectDir.path}'));
}
stdout.writeln('');
stdout.writeln(boldCyan(' ╔══════════════════════════╗'));
stdout.writeln(boldCyan(' ║ nitrogen generate ║'));
stdout.writeln(boldCyan(' ╚══════════════════════════╝'));
stdout.writeln('');
// ── pub get ─────────────────────────────────────────────────────────────
stdout.writeln(cyan(' › flutter pub get …'));
var exitCode = await runStreaming('flutter', ['pub', 'get'], workingDirectory: projectDir.path);
// Exit 255 is a known Dart SDK advisory-decode bug (pub.dev API mismatch).
// Packages are still resolved successfully — do not abort.
if (exitCode != 0 && exitCode != 255) {
stderr.writeln(red(' ✘ flutter pub get failed (exit $exitCode)'));
return exitCode;
}
stdout.writeln('');
// ── build_runner ─────────────────────────────────────────────────────────
// Use `flutter pub run` (not `dart run`) because Flutter projects require
// Flutter's package resolution — `dart run build_runner` fails with
// "Flutter users should use flutter pub instead of dart pub".
//
// Stop any already-running build_runner first. A second invocation hangs
// indefinitely waiting for the lock file; killing the old process and
// removing the lock file lets the new one start immediately.
final existingCount = await killBuildRunner(workingDirectory: projectDir.path);
if (existingCount > 0) {
stdout.writeln(gray(' › Stopped existing build_runner instance.'));
}
// Delete only the lock file and asset graph — NOT the entrypoint/ directory.
// The entrypoint/ directory contains the AOT-compiled builder snapshot; deleting
// it forces an expensive recompile (~10-15 s) on every run. Deleting just the
// lock + asset graph is enough to let build_runner start fresh without
// triggering the "check for updates → dart pub get → exit 247" failure that
// occurs in Flutter workspace members on the second run.
final buildDir = p.join(projectDir.path, '.dart_tool', 'build');
for (final name in ['lock', 'asset_graph.json']) {
final f = File(p.join(buildDir, name));
if (f.existsSync()) f.deleteSync();
}
stdout.writeln(cyan(' › build_runner build …'));
stdout.writeln('');
exitCode = await runStreaming(
'flutter',
[
'pub',
'run',
'build_runner',
'build',
'--delete-conflicting-outputs',
],
workingDirectory: projectDir.path,
);
stdout.writeln('');
if (exitCode != 0) {
stderr.writeln(boldRed(' ✘ build_runner failed (exit $exitCode)'));
stderr.writeln(gray(' Check the output above for details.'));
return exitCode;
}
// ── Post-generation bridge cleanup ───────────────────────────────────────
// Generated Swift bridges live in lib/src/generated/swift/ and are compiled
// via the podspec source_files pattern. Remove any stale copies from Classes/
// to prevent "Invalid redeclaration" Swift compiler errors.
final nitroNativePath = resolveNitroNativePath(projectDir.path);
createSharedHeaders(nitroNativePath, baseDir: projectDir.path);
_syncSwiftBridgesToClasses(projectDir.path);
// ── nitrogen link (auto) ─────────────────────────────────────────────────
// Automatically run the patching logic (build.gradle, Plugin.kt, etc.)
// so users don't have to remember to run `nitrogen link` manually.
stdout.writeln(cyan(' › nitrogen link (auto-patching) …'));
final pluginName = _readPluginName(projectDir.path);
final moduleInfos = discoverModuleInfos(pluginName, baseDir: projectDir.path);
final hasCpp = moduleInfos.any((m) => m.isCpp);
final hasNonCpp = moduleInfos.any((m) => !m.isCpp);
// Patch CMake and C++ stubs
linkCMake(pluginName, moduleInfos.map((m) => m.lib).toList(), nitroNativePath, baseDir: projectDir.path, moduleInfos: moduleInfos);
// Patch iOS/macOS
if (Directory(p.join(projectDir.path, 'ios')).existsSync()) {
linkPodspec(pluginName, moduleInfos.map((m) => m.lib).toList(), baseDir: projectDir.path, moduleInfos: moduleInfos);
if (hasNonCpp) {
final appleCppLibs = moduleInfos.where((m) => isAppleCppModule(File(p.join(projectDir.path, 'lib', 'src', '${m.lib}.native.dart')))).map((m) => m.lib).toSet();
final swiftModules = moduleInfos.where((m) => !appleCppLibs.contains(m.lib)).map((m) => m.toMap()).toList();
linkSwiftPlugin(pluginName, swiftModules, baseDir: projectDir.path);
purgeStaleCppSwiftRegistrations(moduleInfos.where((m) => appleCppLibs.contains(m.lib)).toList(), platform: 'ios', baseDir: projectDir.path);
}
}
if (Directory(p.join(projectDir.path, 'macos')).existsSync()) {
linkMacosPodspec(pluginName, moduleInfos.map((m) => m.lib).toList(), baseDir: projectDir.path, moduleInfos: moduleInfos);
if (hasNonCpp) {
final appleCppLibs = moduleInfos.where((m) => isAppleCppModule(File(p.join(projectDir.path, 'lib', 'src', '${m.lib}.native.dart')))).map((m) => m.lib).toSet();
final swiftModules = moduleInfos.where((m) => !appleCppLibs.contains(m.lib)).map((m) => m.toMap()).toList();
linkMacosSwiftPlugin(pluginName, swiftModules, baseDir: projectDir.path);
purgeStaleCppSwiftRegistrations(moduleInfos.where((m) => appleCppLibs.contains(m.lib)).toList(), platform: 'macos', baseDir: projectDir.path);
}
}
// Patch Android
if (Directory(p.join(projectDir.path, 'android')).existsSync()) {
// Use isAndroidCppModule (android-only) — NOT isNativeCppModule (android+linux).
// A module with 'android: NativeImpl.kotlin, linux: NativeImpl.cpp' still needs
// a Kotlin JniBridge on Android and must not be excluded from kotlinModules.
final androidCppLibs = moduleInfos.where((m) => isAndroidCppModule(File(p.join(projectDir.path, 'lib', 'src', '${m.lib}.native.dart')))).map((m) => m.lib).toSet();
final kotlinModules = moduleInfos.where((m) => !androidCppLibs.contains(m.lib)).map((m) => m.toMap()).toList();
if (kotlinModules.isNotEmpty) {
linkKotlinPlugin(pluginName, kotlinModules, baseDir: projectDir.path);
}
if (hasCpp) {
linkKotlinLoadLibraries(moduleInfos.where((m) => m.isCpp).map((m) => m.lib).toList(), baseDir: projectDir.path);
}
purgeStaleCppKotlinRegistrations(moduleInfos.where((m) => androidCppLibs.contains(m.lib)).toList(), baseDir: projectDir.path);
linkAndroid(pluginName, moduleInfos.map((m) => m.lib).toList(), baseDir: projectDir.path, moduleInfos: moduleInfos);
}
// Patch Desktop
if (Directory(p.join(projectDir.path, 'windows')).existsSync()) {
linkWindows(pluginName, moduleInfos.map((m) => m.lib).toList(), nitroNativePath, baseDir: projectDir.path, moduleInfos: moduleInfos);
}
if (Directory(p.join(projectDir.path, 'linux')).existsSync()) {
linkLinux(pluginName, moduleInfos.map((m) => m.lib).toList(), nitroNativePath, baseDir: projectDir.path, moduleInfos: moduleInfos);
}
linkClangd(pluginName, moduleInfos: moduleInfos, baseDir: projectDir.path);
// ── pod install ──────────────────────────────────────────────────────────
final podfileDirs = findPodfileDirs(projectDir.path);
for (final dir in podfileDirs) {
stdout.writeln(cyan(' › pod install (${p.relative(dir, from: projectDir.path)}) …'));
final podExitCode = await runStreaming(
'pod',
['install'],
workingDirectory: dir,
);
if (podExitCode != 0) {
stderr.writeln(red(' ⚠ pod install failed in $dir (exit $podExitCode) — continuing'));
}
}
// Detect whether any spec uses NativeImpl.cpp to tailor the next-steps hint
final libDir = Directory(p.join(projectDir.path, 'lib'));
final hasCppModules = libDir.existsSync() && libDir.listSync(recursive: true).whereType<File>().where((f) => f.path.endsWith('.native.dart')).any(isCppModule);
stdout.writeln('');
stdout.writeln(boldGreen(' ✨ Generation complete!'));
if (hasCppModules) {
final pubspecName = _readPluginName(projectDir.path);
stdout.writeln(gray(' C++ modules: subclass Hybrid<Module>, call ${pubspecName}_register_impl(&impl).'));
stdout.writeln(gray(' Run nitrogen link to wire bridges into the build system.'));
} else {
stdout.writeln(gray(' Run nitrogen link to wire bridges into the build system.'));
}
stdout.writeln('');
return 0;
}