execute method

Future<int> execute()

Executes the generation logic and returns the exit code. Does NOT call exit().

Implementation

Future<int> execute() async {
  final failOnWarn = argResults!['fail-on-warn'] as bool;

  final projectDir = findNitroProjectRoot();
  if (projectDir == null) {
    _logError('No Nitro project found in . or its subdirectories (must have nitro dependency in pubspec.yaml).');
    return 1;
  }

  if (projectDir.path != Directory.current.path) {
    if (_headless) {
      stdout.writeln('[nitro] project: ${projectDir.path}');
    } else {
      stdout.writeln(gray('  📂 Found project in: ${projectDir.path}'));
    }
  }

  if (_headless) {
    stdout.writeln('[nitro] nitrogen generate');
  } else {
    stdout.writeln('');
    stdout.writeln(boldCyan('  ╔══════════════════════════╗'));
    stdout.writeln(boldCyan('  ║  nitrogen generate       ║'));
    stdout.writeln(boldCyan('  ╚══════════════════════════╝'));
    stdout.writeln('');
  }

  final totalStart = DateTime.now();

  // ── pub get ─────────────────────────────────────────────────────────────
  _log('flutter pub get …');
  final t0 = DateTime.now();
  final pubGetResult = await runStreamingInspected(
    'flutter',
    ['pub', 'get'],
    workingDirectory: projectDir.path,
    headless: _headless,
  );
  _logTiming('pub get', DateTime.now().difference(t0));
  var exitCode = pubGetResult.exitCode;
  // 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) {
    _logError('flutter pub get failed (exit $exitCode)');
    return exitCode;
  }
  if (!_headless) 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) {
    if (_headless) {
      stdout.writeln('[nitro] stopped existing build_runner instance');
    } else {
      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();
  }

  _log('build_runner build …');
  if (!_headless) stdout.writeln('');
  final t1 = DateTime.now();
  final buildResult = await runStreamingInspected(
    'flutter',
    ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs'],
    workingDirectory: projectDir.path,
    headless: _headless,
    scanWarnings: failOnWarn,
  );
  _logTiming('build_runner', DateTime.now().difference(t1));
  exitCode = buildResult.exitCode;

  if (!_headless) stdout.writeln('');
  if (exitCode != 0) {
    _logError('build_runner failed (exit $exitCode)');
    if (!_headless) stderr.writeln(gray('     Check the output above for details.'));
    return exitCode;
  }

  if (failOnWarn && buildResult.hadWarnings) {
    if (_headless) {
      stderr.writeln('[nitro:warn] build_runner emitted warnings — failing due to --fail-on-warn');
    } else {
      stderr.writeln(yellow('  ⚠  Warnings detected. Failing due to --fail-on-warn.'));
    }
    return 2;
  }

  // ── 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.
  _log('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) {
    _log('pod install (${p.relative(dir, from: projectDir.path)}) …');
    final podResult = await runStreamingInspected(
      'pod',
      ['install'],
      workingDirectory: dir,
      headless: _headless,
    );
    if (podResult.exitCode != 0) {
      if (_headless) {
        stderr.writeln('[nitro:warn] pod install failed in $dir (exit ${podResult.exitCode}) — continuing');
      } else {
        stderr.writeln(red('  ⚠  pod install failed in $dir (exit ${podResult.exitCode}) — 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);

  _logTiming('total', DateTime.now().difference(totalStart));

  if (_headless) {
    stdout.writeln('[nitro] generation complete');
    if (hasCppModules) {
      final pubspecName = _readPluginName(projectDir.path);
      stdout.writeln('[nitro] C++ modules: subclass Hybrid<Module>, call ${pubspecName}_register_impl(&impl)');
    }
  } else {
    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;
}