performChecks method
Runs the doctor check logic without launching the UI.
Implementation
DoctorViewResult performChecks({Directory? root}) {
root ??= Directory.current;
final pubspecFile = File(p.join(root.path, 'pubspec.yaml'));
if (!pubspecFile.existsSync()) {
return DoctorViewResult(
pluginName: 'unknown',
sections: [],
errors: 0,
warnings: 0,
errorMessage: 'No pubspec.yaml found. Run from the root of a Flutter plugin.',
);
}
final pluginName = _pluginName(pubspecFile);
final specs = _findSpecs(root: root);
final sections = <DoctorSection>[];
int errors = 0;
int warnings = 0;
void err(DoctorSection s, String label, {String? hint}) {
s.checks.add(DoctorCheck(DoctorStatus.error, label, hint: hint));
errors++;
}
void warn(DoctorSection s, String label, {String? hint}) {
s.checks.add(DoctorCheck(DoctorStatus.warn, label, hint: hint));
warnings++;
}
void ok(DoctorSection s, String label) {
s.checks.add(DoctorCheck(DoctorStatus.ok, label));
}
void info(DoctorSection s, String label) {
s.checks.add(DoctorCheck(DoctorStatus.info, label));
}
// ── System Toolchain ────────────────────────────────────────────────────────
final sysSec = DoctorSection('System Toolchain');
sections.add(sysSec);
// 1. C++ Compiler
try {
final clangResult = Process.runSync('clang++', ['--version']);
if (clangResult.exitCode == 0) {
ok(sysSec, 'clang++ found: ${clangResult.stdout.toString().split('\n').first}');
} else {
warn(sysSec, 'clang++ not found', hint: 'Install build-essential or Xcode Command Line Tools');
}
} catch (_) {
warn(sysSec, 'clang++ not found', hint: 'Install build-essential or Xcode Command Line Tools');
}
// 2. Xcode (on Mac)
if (Platform.isMacOS) {
try {
final xcodeResult = Process.runSync('xcode-select', ['-p']);
if (xcodeResult.exitCode == 0) {
ok(sysSec, 'Xcode at ${xcodeResult.stdout.toString().trim()}');
} else {
err(sysSec, 'Xcode not found', hint: 'Run: xcode-select --install');
}
} catch (_) {
err(sysSec, 'Xcode select failed', hint: 'Run: xcode-select --install');
}
}
// 3. Android NDK
final ndkPath = Platform.environment['ANDROID_NDK_HOME'] ?? Platform.environment['NDK_HOME'];
if (ndkPath != null && Directory(ndkPath).existsSync()) {
ok(sysSec, 'Android NDK: ${p.basename(ndkPath)}');
} else {
// Check local.properties if in an android project, though we are in a plugin...
// Usually users set ANDROID_NDK_HOME globally.
warn(sysSec, 'ANDROID_NDK_HOME not set', hint: 'Set ANDROID_NDK_HOME in your environment');
}
// 4. Java
try {
final javaResult = Process.runSync('java', ['-version']);
// java -version writes to stderr
final javaOut = javaResult.stderr.toString();
if (javaOut.contains('version')) {
ok(sysSec, 'Java: ${javaOut.split('\n').first}');
} else {
warn(sysSec, 'Java not found', hint: 'Install JDK 17+');
}
} catch (_) {
warn(sysSec, 'Java not found', hint: 'Install JDK 17+');
}
final pubSec = DoctorSection('pubspec.yaml');
sections.add(pubSec);
final pubspec = pubspecFile.readAsStringSync();
if (pubspec.contains('nitro:')) {
ok(pubSec, 'nitro dependency present');
} else {
err(pubSec, 'nitro dependency missing', hint: 'Add: nitro: { path: ../packages/nitro }');
}
if (pubspec.contains('build_runner:')) {
ok(pubSec, 'build_runner dev dependency present');
} else {
err(pubSec, 'build_runner dev dependency missing', hint: 'Add to dev_dependencies: build_runner: ^2.4.0');
}
if (pubspec.contains('nitro_generator:')) {
ok(pubSec, 'nitro_generator dev dependency present');
} else {
err(pubSec, 'nitro_generator dev dependency missing', hint: 'Add to dev_dependencies: nitro_generator: { path: ../packages/nitro_generator }');
}
if (RegExp(r'android:\s*\n(?:\s+\S[^\n]*\n)*\s+pluginClass:').hasMatch(pubspec)) {
ok(pubSec, 'android pluginClass defined');
} else {
err(pubSec, 'android pluginClass missing', hint: 'Add pluginClass under flutter.plugin.platforms.android');
}
if (RegExp(r'android:\s*\n(?:\s+\S[^\n]*\n)*\s+package:').hasMatch(pubspec)) {
ok(pubSec, 'android package defined');
} else {
err(pubSec, 'android package missing', hint: 'Add package under flutter.plugin.platforms.android');
}
if (RegExp(r'ios:\s*\n(?:\s+\S[^\n]*\n)*\s+pluginClass:').hasMatch(pubspec)) {
ok(pubSec, 'ios pluginClass defined');
} else if (RegExp(r'ios:\s*\n(?:\s+\S[^\n]*\n)*\s+ffiPlugin:\s*true').hasMatch(pubspec)) {
ok(pubSec, 'ios ffiPlugin: true (pluginClass optional for FFI plugins)');
} else {
err(pubSec, 'ios pluginClass missing', hint: 'Add pluginClass under flutter.plugin.platforms.ios');
}
if (pubspec.contains(' macos:')) {
if (RegExp(r'macos:\s*\n(?:\s+\S[^\n]*\n)*\s+pluginClass:').hasMatch(pubspec)) {
ok(pubSec, 'macos pluginClass defined');
} else if (RegExp(r'macos:\s*\n(?:\s+\S[^\n]*\n)*\s+ffiPlugin:\s*true').hasMatch(pubspec)) {
ok(pubSec, 'macos ffiPlugin: true (pluginClass optional for FFI plugins)');
} else {
warn(pubSec, 'macos pluginClass missing', hint: 'Add pluginClass or ffiPlugin: true under flutter.plugin.platforms.macos');
}
}
if (specs.isNotEmpty) {
final genSec = DoctorSection('Generated Files');
sections.add(genSec);
for (final spec in specs) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final specMtime = spec.lastModifiedSync();
final specIsCpp = isCppModule(spec);
for (final suffix in _generatedSuffixes) {
// .bridge.g.kt is only needed when Android uses Kotlin (not C++).
// .bridge.g.swift is only needed when iOS/macOS uses Swift (not C++).
// Use platform-specific checks instead of the broad isCppModule guard
// so mixed modules (e.g. windows:cpp + android:kotlin) are correctly handled.
if (suffix == '.bridge.g.kt' && !_isAndroidKotlinModule(spec)) {
info(genSec, '${p.basename(spec.path)} → $suffix skipped (android: AndroidNativeImpl.cpp)');
continue;
}
if (suffix == '.bridge.g.swift' && !_isAppleSwiftModule(spec)) {
info(genSec, '${p.basename(spec.path)} → $suffix skipped (ios/macos: AppleNativeImpl.cpp)');
continue;
}
final genPath = _generatedPath(spec.path, stem, suffix);
final genFile = File(genPath);
final relPath = p.relative(genPath);
if (!genFile.existsSync()) {
err(genSec, 'MISSING $relPath', hint: 'Run: nitrogen generate');
} else if (specMtime.isAfter(genFile.lastModifiedSync())) {
warn(genSec, 'STALE $relPath', hint: 'Run: nitrogen generate');
} else {
ok(genSec, relPath);
}
}
// Check cpp-only outputs for NativeImpl.cpp modules.
if (specIsCpp) {
for (final suffix in _cppGeneratedSuffixes) {
final genPath = _generatedPath(spec.path, stem, suffix);
final genFile = File(genPath);
final relPath = p.relative(genPath);
if (!genFile.existsSync()) {
err(genSec, 'MISSING $relPath', hint: 'Run: nitrogen generate');
} else if (specMtime.isAfter(genFile.lastModifiedSync())) {
warn(genSec, 'STALE $relPath', hint: 'Run: nitrogen generate');
} else {
ok(genSec, relPath);
}
}
}
}
} else {
final genSec = DoctorSection('Generated Files');
sections.add(genSec);
warn(genSec, 'No *.native.dart specs found under lib/', hint: 'Create lib/src/<name>.native.dart');
}
final cmakeSec = DoctorSection('CMakeLists.txt');
sections.add(cmakeSec);
final cmakeFile = File(p.join(root.path, 'src', 'CMakeLists.txt'));
if (!cmakeFile.existsSync()) {
err(cmakeSec, 'src/CMakeLists.txt not found', hint: 'Run: nitrogen link');
} else {
final cmake = cmakeFile.readAsStringSync();
// Check for redundant includes in nearby C++ files
final srcDir = Directory(p.join(root.path, 'src'));
final cppFiles = srcDir.listSync().whereType<File>().where((f) => f.path.endsWith('.cpp') || f.path.endsWith('.c')).toList();
for (final f in cppFiles) {
final c = f.readAsStringSync();
if (c.contains('.bridge.g.cpp') || c.contains('.bridge.g.c')) {
err(cmakeSec, 'Redundant bridge include in ${p.basename(f.path)}', hint: 'Remove #include "...bridge.g.cpp" from your source file');
}
}
if (cmake.contains('NITRO_NATIVE')) {
ok(cmakeSec, 'NITRO_NATIVE variable defined');
} else {
warn(cmakeSec, 'NITRO_NATIVE variable missing (incorrect dart_api_dl.c path)', hint: 'Run: nitrogen link');
}
if (cmake.contains('dart_api_dl.c')) {
ok(cmakeSec, 'dart_api_dl.c included');
} else {
err(cmakeSec, 'dart_api_dl.c not included', hint: 'Run: nitrogen link');
}
// Build a lookup: impl file name → whether it's a native-cpp (android/linux)
// module so we can skip “unlinked source” warnings for files that are
// intentionally absent from the Android CMakeLists.txt (windows-only cpp).
final nativeCppImplFiles = <String>{};
for (final spec in specs) {
if (!isNativeCppModule(spec)) continue;
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final moduleMatch = RegExp(r'abstract class (\w+) extends HybridObject').firstMatch(spec.readAsStringSync());
final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
nativeCppImplFiles.add('Hybrid$moduleName.cpp');
}
// Check for unlinked source files in src/.
// Skip HybridXxx.cpp files for modules that are NOT native-cpp (android/linux) —
// e.g. a module that is only C++ on Windows has its impl in windows/CMakeLists.txt.
final allSrcFiles = srcDir.listSync().whereType<File>().where((f) => f.path.endsWith('.cpp') || f.path.endsWith('.c')).toList();
for (final f in allSrcFiles) {
final name = p.basename(f.path);
if (name == 'dart_api_dl.c') continue;
if (name == '$pluginName.cpp' || name == '$pluginName.c') continue;
// Hybrid impl files for windows-only cpp modules don’t belong in the
// Android/Linux CMakeLists — skip them to avoid a false-positive warning.
if (name.startsWith('Hybrid') && name.endsWith('.cpp') && !nativeCppImplFiles.contains(name)) continue;
if (!cmake.contains('"$name"') && !cmake.contains(' $name ') && !cmake.contains('\n $name')) {
warn(cmakeSec, 'Unlinked source: $name', hint: 'File found in src/ but not mentioned in CMakeLists.txt');
}
}
for (final spec in specs) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final lib = _extractLibName(spec) ?? stem.replaceAll('-', '_');
if (cmake.contains('add_library($lib ')) {
ok(cmakeSec, 'add_library($lib) target present');
// Verify HybridXxx.cpp is linked for native-cpp (android/linux) modules.
// Windows-only cpp modules do NOT need this in src/CMakeLists.txt.
if (isNativeCppModule(spec)) {
final moduleMatch = RegExp(r'abstract class (\w+) extends HybridObject').firstMatch(spec.readAsStringSync());
final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
final implName = 'Hybrid$moduleName.cpp';
if (!cmake.contains('"$implName"') && !cmake.contains(' $implName ') && !cmake.contains('\n $implName')) {
err(cmakeSec, '$lib: $implName not linked in target', hint: 'Add "$implName" to add_library($lib ...)');
}
}
} else {
err(cmakeSec, 'add_library($lib) missing', hint: 'Run: nitrogen link');
}
}
}
// Whether any / all specs use NativeImpl.cpp — used below to skip irrelevant checks.
final allSpecsCpp = specs.isNotEmpty && specs.every(isCppModule);
final hasAnyCppSpec = specs.any(isCppModule);
final hasAnyNonCppSpec = specs.any((s) => !isCppModule(s));
final androidSec = DoctorSection('Android');
sections.add(androidSec);
final androidDir = Directory(p.join(root.path, 'android'));
if (!androidDir.existsSync()) {
info(androidSec, 'android/ directory not present — skipped');
} else if (allSpecsCpp) {
// Pure C++ plugin — no Kotlin bridge needed.
info(androidSec, 'All modules use NativeImpl.cpp — Kotlin JNI bridge not required');
// Still check that the NDK can build the shared library.
final gradle = File(p.join(androidDir.path, 'build.gradle'));
if (gradle.existsSync() && gradle.readAsStringSync().contains('externalNativeBuild')) {
ok(androidSec, 'externalNativeBuild configured (NDK build)');
} else {
info(androidSec, 'Add externalNativeBuild to android/build.gradle if using CMake directly');
}
} else {
final gradle = File(p.join(androidDir.path, 'build.gradle'));
if (!gradle.existsSync()) {
err(androidSec, 'android/build.gradle not found');
} else {
final g = gradle.readAsStringSync();
if (g.contains('"kotlin-android"') || g.contains("'kotlin-android'")) {
ok(androidSec, 'kotlin-android plugin applied');
} else {
err(androidSec, 'kotlin-android plugin missing', hint: 'Add: apply plugin: "kotlin-android"');
}
if (g.contains('kotlinOptions')) {
ok(androidSec, 'kotlinOptions block present');
} else {
err(androidSec, 'kotlinOptions block missing', hint: 'Add: kotlinOptions { jvmTarget = "17" }');
}
if (g.contains('generated/kotlin')) {
ok(androidSec, 'generated/kotlin sourceSets entry present');
// Warn if java.srcDirs also points at the generated kotlin directory.
// In AGP 8.x this routes .kt files through the Java compiler path and
// causes "Unresolved reference: XxxJniBridge" compile errors.
if (RegExp(r'java\.srcDirs\s*\+=.*generated/kotlin').hasMatch(g)) {
err(
androidSec,
'java.srcDirs includes generated/kotlin — causes "Unresolved reference: XxxJniBridge" in AGP 8.x',
hint: 'Remove the java.srcDirs line; kotlin.srcDirs alone is sufficient',
);
}
} else {
err(androidSec, 'sourceSets entry for generated/kotlin missing',
hint: 'Add: kotlin.srcDirs += "\${project.projectDir}/../lib/src/generated/kotlin"');
}
if (g.contains('kotlinx-coroutines')) {
ok(androidSec, 'kotlinx-coroutines dependency present');
} else {
err(androidSec, 'kotlinx-coroutines missing in dependencies');
}
}
final ktDir = Directory(p.join(androidDir.path, 'src', 'main', 'kotlin'));
final pluginFiles = ktDir.existsSync() ? ktDir.listSync(recursive: true).whereType<File>().where((f) => f.path.endsWith('Plugin.kt')).toList() : <File>[];
if (pluginFiles.isEmpty) {
err(androidSec, 'No Plugin.kt found', hint: 'Run: nitrogen init');
} else {
final kt = pluginFiles.first.readAsStringSync();
// Only check System.loadLibrary for non-cpp specs (cpp libs are also loaded but that's fine)
for (final spec in specs) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final lib = _extractLibName(spec) ?? stem.replaceAll('-', '_');
if (kt.contains('System.loadLibrary("$lib")')) {
ok(androidSec, 'System.loadLibrary("$lib") in Plugin.kt');
} else {
err(androidSec, 'System.loadLibrary("$lib") missing', hint: 'Run: nitrogen link');
}
}
// JniBridge.register only needed for non-cpp specs
if (hasAnyNonCppSpec) {
if (kt.contains('JniBridge.register(')) {
ok(androidSec, 'JniBridge.register(...) call present');
} else {
warn(androidSec, 'JniBridge.register(...) not found in Plugin.kt', hint: 'Add register call in onAttachedToEngine');
}
} else {
info(androidSec, 'JniBridge.register not needed — all modules use NativeImpl.cpp');
}
// Check for stale JniBridge.register() calls for C++ modules.
// When a module transitions from Kotlin/JNI to NativeImpl.cpp its
// JniBridge class no longer exists, causing "Unresolved reference" at
// compile time. nitrogen link auto-removes these, but doctor flags them
// so users know to re-run link.
for (final spec in specs.where(isCppModule)) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final moduleMatch = RegExp(r'abstract class (\w+) extends HybridObject').firstMatch(spec.readAsStringSync());
final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
if (kt.contains('${moduleName}JniBridge.register(')) {
err(
androidSec,
'Stale ${moduleName}JniBridge.register() in Plugin.kt — $moduleName is now NativeImpl.cpp',
hint: 'Run: nitrogen link (auto-removes stale registrations for C++ modules)',
);
}
}
// For each non-cpp Kotlin module, verify the JniBridge import is present.
// Missing imports cause "Unresolved reference: FooJniBridge" Kotlin errors.
// nitrogen link auto-injects these imports alongside the register() call.
for (final spec in specs.where((s) => !isCppModule(s))) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final lib = (_extractLibName(spec) ?? stem).replaceAll('-', '_');
final moduleMatch = RegExp(r'abstract class (\w+) extends HybridObject').firstMatch(spec.readAsStringSync());
final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
final importLine = 'import nitro.${lib}_module.${moduleName}JniBridge';
if (!kt.contains(importLine)) {
err(
androidSec,
'Missing import in Plugin.kt: $importLine',
hint: 'Run: nitrogen link (auto-adds missing JniBridge imports)',
);
} else {
ok(androidSec, 'import ${moduleName}JniBridge present');
}
}
}
}
final iosSec = DoctorSection('iOS');
sections.add(iosSec);
final iosDir = Directory(p.join(root.path, 'ios'));
if (!iosDir.existsSync()) {
info(iosSec, 'ios/ directory not present — skipped');
} else {
final podFiles = iosDir.listSync().whereType<File>().where((f) => f.path.endsWith('.podspec')).toList();
if (podFiles.isEmpty) {
err(iosSec, 'No .podspec found in ios/', hint: 'Run: nitrogen init');
} else {
final pod = podFiles.first.readAsStringSync();
final podName = p.basename(podFiles.first.path);
if (pod.contains("s.dependency 'nitro'")) {
ok(iosSec, "s.dependency 'nitro' in $podName");
} else {
err(iosSec, "s.dependency 'nitro' missing in $podName", hint: 'Run: nitrogen link');
}
if (pod.contains('HEADER_SEARCH_PATHS')) {
ok(iosSec, 'HEADER_SEARCH_PATHS in $podName');
} else {
err(iosSec, 'HEADER_SEARCH_PATHS missing in $podName', hint: 'Run: nitrogen link');
}
if (pod.contains('c++17')) {
ok(iosSec, 'CLANG_CXX_LANGUAGE_STANDARD = c++17');
} else {
warn(iosSec, 'CLANG_CXX_LANGUAGE_STANDARD not set to c++17', hint: "Set: 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17' in pod_target_xcconfig");
}
if (!allSpecsCpp) {
// swift_version only relevant when Swift bridges are used
if (pod.contains("swift_version = '5.9'") || pod.contains("swift_version = '6")) {
ok(iosSec, 'swift_version ≥ 5.9');
} else {
warn(iosSec, 'swift_version may be too old', hint: "Set: s.swift_version = '5.9'");
}
}
// Check for complete HEADER_SEARCH_PATHS
if (pod.contains('lib/src/generated/cpp') && pod.contains('src/native')) {
ok(iosSec, 'Comprehensive HEADER_SEARCH_PATHS in podspec');
} else {
warn(iosSec, 'Incomplete HEADER_SEARCH_PATHS in podspec', hint: 'Run: nitrogen link');
}
}
final classesDir = Directory(p.join(iosDir.path, 'Classes'));
if (allSpecsCpp) {
// All C++ modules — no Swift Registry.register() needed.
info(iosSec, 'All modules use NativeImpl.cpp — Swift bridge (Registry.register) not required');
// .native.g.h uses C++ types (std::string, classes) and must NOT be placed in
// ios/Classes/ — CocoaPods includes every header there into the umbrella header
// which breaks Swift/ObjC compilation. It is reachable via HEADER_SEARCH_PATHS.
// Verify that HEADER_SEARCH_PATHS includes lib/src/generated/cpp/ instead.
final podFiles = iosDir.listSync().whereType<File>().where((f) => f.path.endsWith('.podspec')).toList();
if (podFiles.isNotEmpty) {
final pod = podFiles.first.readAsStringSync();
if (pod.contains('lib/src/generated/cpp')) {
ok(iosSec, '*.native.g.h reachable via HEADER_SEARCH_PATHS → lib/src/generated/cpp');
} else {
warn(iosSec, 'HEADER_SEARCH_PATHS may not include lib/src/generated/cpp (needed for *.native.g.h)', hint: 'Run: nitrogen link');
}
}
} else {
final swiftFiles = classesDir.existsSync()
? classesDir.listSync().whereType<File>().where((f) => f.path.endsWith('Plugin.swift')).toList()
: <File>[];
if (swiftFiles.isEmpty) {
err(iosSec, 'No *Plugin.swift in ios/Classes/', hint: 'Run: nitrogen init');
} else {
final swift = swiftFiles.first.readAsStringSync();
if (hasAnyNonCppSpec) {
if (swift.contains('Registry.register(') || swift.contains('.register(')) {
ok(iosSec, 'Plugin.swift has Registry.register(...)');
} else {
warn(iosSec, 'Registry.register(...) not found in Plugin.swift', hint: 'Add: NitroModules.Registry.register(...) in register(with:)');
}
} else {
info(iosSec, 'Registry.register not needed — all modules use NativeImpl.cpp');
}
// Check for stale XxxRegistry.register() calls for C++ modules.
// AppleNativeImpl.cpp modules have no Swift Registry — the call causes
// "Cannot find 'XxxRegistry' in scope". nitrogen link auto-removes these.
for (final spec in specs.where(isCppModule)) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final moduleMatch = RegExp(r'abstract class (\w+) extends HybridObject').firstMatch(spec.readAsStringSync());
final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
if (swift.contains('${moduleName}Registry.register(')) {
err(
iosSec,
'Stale ${moduleName}Registry.register() in Plugin.swift — $moduleName is now NativeImpl.cpp',
hint: 'Run: nitrogen link (auto-removes stale Swift registry calls for C++ modules)',
);
}
}
}
}
final dartApiDl = File(p.join(iosDir.path, 'Classes', 'dart_api_dl.c'));
if (dartApiDl.existsSync()) {
ok(iosSec, 'ios/Classes/dart_api_dl.c present');
} else {
err(iosSec, 'ios/Classes/dart_api_dl.c missing', hint: 'Run: nitrogen link');
}
final nitroH = File(p.join(iosDir.path, 'Classes', 'nitro.h'));
if (nitroH.existsSync()) {
ok(iosSec, 'ios/Classes/nitro.h present');
} else {
err(iosSec, 'ios/Classes/nitro.h missing', hint: 'Run: nitrogen link');
}
if (nitroH.existsSync()) {
final content = nitroH.readAsStringSync();
if (content.contains('NITRO_EXPORT')) {
ok(iosSec, 'nitro.h contains NITRO_EXPORT visibility macro');
} else {
err(iosSec, 'nitro.h missing NITRO_EXPORT visibility macro', hint: 'Run: nitrogen link');
}
}
// Bridge files must use .mm (Objective-C++) not .cpp (pure C++).
// .cpp files cause __OBJC__ to be undefined, making @try/@catch dead
// code — NSException from Swift propagates uncaught and crashes the app.
final staleCppBridges = classesDir.existsSync() ? classesDir.listSync().whereType<File>().where((f) => f.path.endsWith('.bridge.g.cpp')).toList() : <File>[];
if (staleCppBridges.isNotEmpty) {
for (final f in staleCppBridges) {
err(iosSec, 'Stale .cpp bridge: ${p.basename(f.path)} (must be .mm)', hint: 'Run: nitrogen link (auto-renames .bridge.g.cpp → .bridge.g.mm)');
}
}
final mmBridges = classesDir.existsSync() ? classesDir.listSync().whereType<File>().where((f) => f.path.endsWith('.bridge.g.mm')).toList() : <File>[];
if (mmBridges.isNotEmpty) {
ok(iosSec, '${mmBridges.length} .bridge.g.mm file(s) in ios/Classes/');
} else if (specs.isNotEmpty && !allSpecsCpp) {
// Only warn about missing .mm bridges for non-cpp modules
warn(iosSec, 'No .bridge.g.mm files in ios/Classes/', hint: 'Run: nitrogen link');
}
}
final macosSec = DoctorSection('macOS');
sections.add(macosSec);
final macosDir = Directory(p.join(root.path, 'macos'));
if (!macosDir.existsSync()) {
info(macosSec, 'macos/ directory not present — skipped');
} else {
final podFiles = macosDir.listSync().whereType<File>().where((f) => f.path.endsWith('.podspec')).toList();
if (podFiles.isEmpty) {
err(macosSec, 'No .podspec found in macos/', hint: 'Run: nitrogen init');
} else {
final pod = podFiles.first.readAsStringSync();
final podName = p.basename(podFiles.first.path);
if (pod.contains("s.dependency 'nitro'")) {
ok(macosSec, "s.dependency 'nitro' in $podName");
} else {
err(macosSec, "s.dependency 'nitro' missing in $podName", hint: 'Run: nitrogen link');
}
if (pod.contains('HEADER_SEARCH_PATHS')) {
ok(macosSec, 'HEADER_SEARCH_PATHS in $podName');
} else {
err(macosSec, 'HEADER_SEARCH_PATHS missing in $podName', hint: 'Run: nitrogen link');
}
if (pod.contains('c++17')) {
ok(macosSec, 'CLANG_CXX_LANGUAGE_STANDARD = c++17');
} else {
warn(macosSec, 'CLANG_CXX_LANGUAGE_STANDARD not set to c++17', hint: "Set: 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17' in pod_target_xcconfig");
}
if (pod.contains('lib/src/generated/cpp') && pod.contains('src/native')) {
ok(macosSec, 'Comprehensive HEADER_SEARCH_PATHS in podspec');
} else {
warn(macosSec, 'Incomplete HEADER_SEARCH_PATHS in podspec', hint: 'Run: nitrogen link');
}
}
final macosClassesDir = Directory(p.join(macosDir.path, 'Classes'));
if (allSpecsCpp) {
info(macosSec, 'All modules use NativeImpl.cpp — Swift bridge (Registry.register) not required');
final cppHeaders = macosClassesDir.existsSync() ? macosClassesDir.listSync().whereType<File>().where((f) => f.path.endsWith('.native.g.h')).toList() : <File>[];
if (cppHeaders.isNotEmpty) {
ok(macosSec, '${cppHeaders.length} *.native.g.h header(s) synced to macos/Classes/');
} else if (hasAnyCppSpec) {
warn(macosSec, 'No *.native.g.h in macos/Classes/', hint: 'Run: nitrogen generate && nitrogen link');
}
} else {
final swiftFiles = macosClassesDir.existsSync() ? macosClassesDir.listSync().whereType<File>().where((f) => f.path.endsWith('Plugin.swift')).toList() : <File>[];
if (swiftFiles.isEmpty) {
err(macosSec, 'No *Plugin.swift in macos/Classes/', hint: 'Run: nitrogen init');
} else {
final swift = swiftFiles.first.readAsStringSync();
if (hasAnyNonCppSpec) {
if (swift.contains('Registry.register(') || swift.contains('.register(')) {
ok(macosSec, 'Plugin.swift has Registry.register(...)');
} else {
warn(macosSec, 'Registry.register(...) not found in Plugin.swift', hint: 'Add: NitroModules.Registry.register(...) in register(with:)');
}
} else {
info(macosSec, 'Registry.register not needed — all modules use NativeImpl.cpp');
}
}
}
final dartApiDl = File(p.join(macosDir.path, 'Classes', 'dart_api_dl.c'));
if (dartApiDl.existsSync()) {
ok(macosSec, 'macos/Classes/dart_api_dl.c present');
} else {
err(macosSec, 'macos/Classes/dart_api_dl.c missing', hint: 'Run: nitrogen link');
}
final nitroH = File(p.join(macosDir.path, 'Classes', 'nitro.h'));
if (nitroH.existsSync()) {
ok(macosSec, 'macos/Classes/nitro.h present');
} else {
err(macosSec, 'macos/Classes/nitro.h missing', hint: 'Run: nitrogen link');
}
if (nitroH.existsSync()) {
final content = nitroH.readAsStringSync();
if (content.contains('NITRO_EXPORT')) {
ok(macosSec, 'nitro.h contains NITRO_EXPORT visibility macro');
} else {
err(macosSec, 'nitro.h missing NITRO_EXPORT visibility macro', hint: 'Run: nitrogen link');
}
}
final staleCppBridges = macosClassesDir.existsSync() ? macosClassesDir.listSync().whereType<File>().where((f) => f.path.endsWith('.bridge.g.cpp')).toList() : <File>[];
if (staleCppBridges.isNotEmpty) {
for (final f in staleCppBridges) {
err(macosSec, 'Stale .cpp bridge: ${p.basename(f.path)} (must be .mm)', hint: 'Run: nitrogen link (auto-renames .bridge.g.cpp → .bridge.g.mm)');
}
}
final mmBridges = macosClassesDir.existsSync() ? macosClassesDir.listSync().whereType<File>().where((f) => f.path.endsWith('.bridge.g.mm')).toList() : <File>[];
if (mmBridges.isNotEmpty) {
ok(macosSec, '${mmBridges.length} .bridge.g.mm file(s) in macos/Classes/');
} else if (specs.isNotEmpty && !allSpecsCpp) {
warn(macosSec, 'No .bridge.g.mm files in macos/Classes/', hint: 'Run: nitrogen link');
}
}
// ── Windows ──────────────────────────────────────────────────────────────
final winSec = DoctorSection('Windows');
sections.add(winSec);
final winDir = Directory(p.join(root.path, 'windows'));
if (!winDir.existsSync()) {
info(winSec, 'windows/ directory not present — skipped');
} else {
final cmakeFile = File(p.join(winDir.path, 'CMakeLists.txt'));
if (!cmakeFile.existsSync()) {
err(winSec, 'windows/CMakeLists.txt not found', hint: 'Run: nitrogen link');
} else {
final cmake = cmakeFile.readAsStringSync();
if (cmake.contains('NITRO_NATIVE')) {
ok(winSec, 'NITRO_NATIVE variable defined in windows/CMakeLists.txt');
} else {
err(winSec, 'NITRO_NATIVE missing in windows/CMakeLists.txt', hint: 'Run: nitrogen link');
}
if (cmake.contains('dart_api_dl.c')) {
ok(winSec, 'dart_api_dl.c included in windows/CMakeLists.txt');
} else {
err(winSec, 'dart_api_dl.c not included in windows/CMakeLists.txt', hint: 'Run: nitrogen link');
}
for (final spec in specs) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final lib = _extractLibName(spec) ?? stem.replaceAll('-', '_');
final bridgeRel = '../lib/src/generated/cpp/$lib.bridge.g.cpp';
if (cmake.contains(bridgeRel)) {
ok(winSec, '$lib.bridge.g.cpp linked in windows/CMakeLists.txt');
} else {
warn(winSec, '$lib.bridge.g.cpp not linked in windows/CMakeLists.txt', hint: 'Run: nitrogen link');
}
}
}
}
// ── Linux ─────────────────────────────────────────────────────────────────
final linuxSec = DoctorSection('Linux');
sections.add(linuxSec);
final linuxDir = Directory(p.join(root.path, 'linux'));
if (!linuxDir.existsSync()) {
info(linuxSec, 'linux/ directory not present — skipped');
} else {
final cmakeFile = File(p.join(linuxDir.path, 'CMakeLists.txt'));
if (!cmakeFile.existsSync()) {
err(linuxSec, 'linux/CMakeLists.txt not found', hint: 'Run: nitrogen link');
} else {
final cmake = cmakeFile.readAsStringSync();
if (cmake.contains('NITRO_NATIVE')) {
ok(linuxSec, 'NITRO_NATIVE variable defined in linux/CMakeLists.txt');
} else {
err(linuxSec, 'NITRO_NATIVE missing in linux/CMakeLists.txt', hint: 'Run: nitrogen link');
}
if (cmake.contains('dart_api_dl.c')) {
ok(linuxSec, 'dart_api_dl.c included in linux/CMakeLists.txt');
} else {
err(linuxSec, 'dart_api_dl.c not included in linux/CMakeLists.txt', hint: 'Run: nitrogen link');
}
for (final spec in specs) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final lib = _extractLibName(spec) ?? stem.replaceAll('-', '_');
final bridgeRel = '../lib/src/generated/cpp/$lib.bridge.g.cpp';
if (cmake.contains(bridgeRel)) {
ok(linuxSec, '$lib.bridge.g.cpp linked in linux/CMakeLists.txt');
} else {
warn(linuxSec, '$lib.bridge.g.cpp not linked in linux/CMakeLists.txt', hint: 'Run: nitrogen link');
}
}
}
}
// ── NativeImpl.cpp Direct Implementation ────────────────────────────────
if (hasAnyCppSpec) {
final cppSec = DoctorSection('NativeImpl.cpp Direct Implementation');
sections.add(cppSec);
for (final spec in specs.where(isCppModule)) {
final stem = p.basename(spec.path).replaceAll(RegExp(r'\.native\.dart$'), '');
final lib = _extractLibName(spec) ?? stem.replaceAll('-', '_');
final moduleMatch = RegExp(r'abstract class (\w+) extends HybridObject').firstMatch(spec.readAsStringSync());
final parsedSegments = stem.split('_').where((w) => w.isNotEmpty).toList();
final fallbackName = parsedSegments.isNotEmpty ? parsedSegments.map((w) => w[0].toUpperCase() + w.substring(1)).join('') : lib;
final moduleName = moduleMatch?.group(1) ?? fallbackName;
// Check if user has a C++ impl file in src/ (anything that isn't generated or dart_api_dl)
final srcDir = Directory(p.join(root.path, 'src'));
final cppImplFiles = srcDir.existsSync()
? srcDir
.listSync()
.whereType<File>()
.where((f) => f.path.endsWith('.cpp') && !f.path.contains('.bridge.g.') && !f.path.contains('.test.g.') && !f.path.contains('dart_api_dl'))
.toList()
: <File>[];
if (cppImplFiles.isNotEmpty) {
// Check if any impl file registers the implementation
final anyRegisters = cppImplFiles.any((f) => f.readAsStringSync().contains('${lib}_register_impl'));
if (anyRegisters) {
ok(cppSec, '$lib: ${lib}_register_impl() wired up in user impl');
} else {
warn(cppSec, '$lib: ${lib}_register_impl(&impl) not found in src/', hint: 'Call ${lib}_register_impl(&impl) at startup before first Dart use');
}
} else {
info(cppSec, '$lib: Create src/Hybrid$moduleName.cpp, subclass Hybrid$moduleName, then call ${lib}_register_impl(&impl)');
}
// Check .clangd includes the test/ directory (for GoogleMock IDE support)
final clangdFile = File(p.join(root.path, '.clangd'));
if (clangdFile.existsSync() && clangdFile.readAsStringSync().contains('generated/cpp/test')) {
ok(cppSec, '.clangd includes generated/cpp/test/ (GoogleMock IDE support)');
} else {
info(cppSec, 'Run: nitrogen link (adds generated/cpp/test/ to .clangd for IDE mock support)');
}
}
}
return DoctorViewResult(
pluginName: pluginName,
sections: sections,
errors: errors,
warnings: warnings,
);
}