execute method

Future<int> execute()

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;
}