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 (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();
for (final suffix in _generatedSuffixes) {
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();
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');
}
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');
} else {
err(cmakeSec, 'add_library($lib) missing', hint: 'Run: nitrogen link');
}
}
}
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 {
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');
} else {
err(androidSec, 'sourceSets entry for generated/kotlin missing');
}
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();
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');
}
}
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');
}
}
}
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('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 (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'");
}
}
final classesDir = Directory(p.join(iosDir.path, 'Classes'));
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 (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:)');
}
}
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');
}
// 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) {
warn(iosSec, 'No .bridge.g.mm files in ios/Classes/', hint: 'Run: nitrogen link');
}
}
return DoctorViewResult(
pluginName: pluginName,
sections: sections,
errors: errors,
warnings: warnings,
);
}