KDoctor: Your Ultimate Troubleshooting Companion for the Kotlin Multiplatform Ecosystem
Tools|Apr 19, 2023|Last edited: May 4, 2023
Tags:
Tool
Kotlin
Compose Multiplatform
Kotlin Multiplatform

type
Post
status
Published
date
Apr 19, 2023
slug
kdoctor-environment-analysis-tool-kotlin-multiplatform
summary
Discover KDoctor, a powerful tool designed to diagnose and resolve common issues in the Compose / Kotlin Multiplatform ecosystem, ensuring a smoother development experience.
tags
Tool
Kotlin
Compose Multiplatform
Kotlin Multiplatform
category
Tools
icon
password
What is KDoctor?
Developing applications with Kotlin has grown increasingly popular, thanks to its concise syntax, interoperability with Java, and robust ecosystem. With
Compose / Kotlin Multiplatform
gaining attention, as with any programming language, developers may encounter issues related to Kotlin and its related technologies, such as Gradle
Android Studio
, Xcode
and CocoaPods
. That's where KDoctor comes in! This powerful tool is designed to help developers diagnose and resolve common problems, ensuring a smoother development experience.KDoctor is an Environment analysis tool intended for Kotlin Multiplatform work station setup.
In this blog post, we'll explore the inner workings of
KDoctor
and its key components.Installing KDoctor
KDoctor is available for macOS only.
Install:
brew install kdoctor
To run KDoctor, follow these steps:
- Install KDoctor.
- Open a terminal window.
- Type
kdoctor
and press Enter.
- Wait for KDoctor to complete the diagnostics. The output will show whether your system is ready for Kotlin Multiplatform Mobile development.

As seen in the above picture, notice how
kdoctor
has identified certain issues related to CocoaPods
and provided information on other necessary tools!KDoctor - The Tool Overview
Each diagnostic class extends the
Diagnostic
class and implements the diagnose()
method, which returns a Diagnosis
object that contains information about the diagnostic result. The System
class provides methods for executing shell commands, finding files and directories, and reading system environment variables, which are used by the diagnostic classes.Some important parts of the code include:
findAndroidStudio()
method inAndroidStudioDiagnostic
class, which searches for Android Studio installations and filters out backups created by the JetBrains Toolbox.
findGradleWrapper()
method inGradleDiagnostic
class, which searches for Gradle wrapper scripts in the current directory and its parent directories, and returns the path of the first script found.
CocoapodsDiagnostic
class, which checks the Ruby and Ruby Gems installations, and verifies the installation and version of CocoaPods.
JavaDiagnostic
class, which checks whether Java is installed and whether theJAVA_HOME
environment variable is set correctly.
findXcode
function takes a path to an Xcode installation and returns anApplication
object representing the Xcode installation. Thediagnose
function uses this function to find all Xcode installations on the system.
- The main entry point of the
KDoctor
tool. Themain
function instantiates and runs aMain
object, which is aCliktCommand
object that parses the command line arguments using theClikt
library.
Overall, the code demonstrates the use of Kotlin to develop a useful tool for diagnosing issues related to the Multiplatform ecosystem.
Conclusion
KDoctor is an invaluable tool for diagnosing and resolving issues related to Kotlin and its Multiplatform ecosystem. With its set of diagnostic classes and useful methods, KDoctor ensures that developers have a working environment to get started with Multiplatform development.
Reference
KDoctor:
kdoctor
Kotlin • Updated May 29, 2023
Some Usage Examples in README: https://github.com/JetBrains/compose-multiplatform-ios-android-template
Clikt - command line library:
clikt
ajalt • Updated May 26, 2023
Source Code Explanation: KDoctor Tool by Kotlin
actual fun getSystem(): System = MacosSystem()
This code is a Kotlin file containing a function called
getSystem()
with an actual implementation for macOS systems.Expect-actual declarations are a Kotlin language feature that allows a library to define an abstract interface or API and then provide actual implementations of that API for specific platforms. In this case, the expect declaration is defined elsewhere and the actual implementation for macOS systems is provided here.
The
getSystem()
function returns an instance of the MacosSystem
class which implements the System
interface. The System
interface is also defined elsewhere in the codebase and is likely used to represent information about the current system or environment.class Doctor(private val system: System) { fun diagnoseKmmEnvironment( verbose: Boolean, debug: Boolean, extraDiagnostics: Boolean, localCompatibilityJson: String?, templateProjectTag: String? ): Flow<String> = channelFlow { if (verbose) { send("Environment diagnose:\\n") } else { send("Environment diagnose (to see all details, use -v option):\\n") } ... } }
This is the implementation of the
Doctor
class, which is the main class of the KDoctor
tool.internal const val KDOCTOR_VERSION = "1.0.0" fun main(args: Array<String>) = Main().main(args) private class Main : CliktCommand(name = "kdoctor") { val showVersion: Boolean by option( "--version", help = "Report a version of KDoctor" ).flag() val isVerbose: Boolean by option( "--verbose", "-v", help = "Report an extended information" ).flag() val isExtraDiagnostics: Boolean by option( "--all", "-a", help = "Run extra diagnostics such as a build of a synthetic project and an analysis of a project in the current directory" ).flag() val showDevelopmentTeams: Boolean by option( "--team-ids", help = "Report all available Apple dev team ids" ).flag() val isDebug: Boolean by option("--debug", hidden = true).flag() val localCompatibilityJson: String? by option("--compatibilityJson", hidden = true) val templateProjectTag: String? by option("--templateProject", hidden = true) override fun run() { ... } } expect fun getSystem(): System
This is the main entry point of the
KDoctor
tool. The main
function instantiates and runs a Main
object, which is a CliktCommand
object that parses the command line arguments using the Clikt
library.enum class OS(private val str: String) { MacOS("macOS"), Windows("Windows"), Linux("Linux"); override fun toString() = str } enum class Shell(val path: String, val profile: String) { Bash("/bin/bash", "~/.bash_profile"), Zsh("/bin/zsh", "~/.zprofile") } data class ProcessResult(val code: Int, val rawOutput: String?) { val output get() = if (code == 0) rawOutput else null } interface System { val currentOS: OS val osVersion: Version? val cpuInfo: String? val homeDir: String val shell: Shell? fun getEnvVar(name: String): String? fun execute(command: String, vararg args: String): ProcessResult fun fileExists(path: String): Boolean fun readFile(path: String): String? fun writeTempFile(content: String): String fun readArchivedFile(pathToArchive: String, pathToFile: String): String? fun findAppsPathsInDirectory(prefix: String, directory: String, recursively: Boolean = false): List<String> } fun System.isUsingRosetta() = execute("sysctl", "sysctl.proc_translated").output ?.substringAfter("sysctl.proc_translated: ") ?.toIntOrNull() == 1 fun System.isUsingM1() = cpuInfo?.contains("Apple") == true fun System.parsePlist(path: String): Map<String, Any>? { ... } fun System.spotlightFindAppPaths(appId: String): List<String> = execute("/usr/bin/mdfind", "kMDItemCFBundleIdentifier=\\"$appId\\"").output ?.split("\\n") ?.filter { it.isNotBlank() } .orEmpty()
This code defines a set of interfaces and utility functions for interacting with the system, such as executing shell commands and finding files. It also defines two enums,
OS
and Shell
, representing the operating system and the shell respectively.The
isUsingRosetta()
and isUsingM1()
extension functions are used to determine if the system is using the Rosetta translation layer or if it's an M1-based Mac. The parsePlist()
function is used to parse the contents of a .plist file, commonly used in macOS applications, into a Map<String, Any>
object.Finally, the
spotlightFindAppPaths()
function uses the mdfind
command-line tool on macOS to find the paths to all installed applications with a given bundle identifier.object TextPainter { const val RESET = "\\u001B[0m" const val RED = "\\u001B[31m" const val GREEN = "\\u001B[32m" const val YELLOW = "\\u001B[33m" const val BOLD = "\\u001B[1m" }
This file contains a simple object
TextPainter
which provides some constants for ANSI escape codes used to colorize text in the console.class XcodeDiagnostic(private val system: System) : Diagnostic() { override val title = "Xcode" override fun diagnose(): Diagnosis { ... } private fun findXcode(path: String): Application? { Logger.d("findXcode($path)") val plist = system.parsePlist("$path/Contents/Info.plist") ?: return null val version = plist["CFBundleShortVersionString"]?.toString()?.trim('"') ?: return null val name = plist["CFBundleName"]?.toString() ?.trim('"') ?: path.substringAfterLast("/").substringBeforeLast(".") return Application(name, Version(version), path) } }
The
findXcode
function takes a path to an Xcode installation and returns an Application
object representing the Xcode installation. The diagnose
function uses this function to find all Xcode installations on the system.The
diagnose
function first attempts to find an Xcode installation using Spotlight search. If no Xcode installation is found, it searches for Xcode in the /Applications
and ~/Applications
directories.If multiple Xcode installations are found, the function adds an
INFO
diagnosis entry indicating that multiple installations were found, and it also retrieves the current command line tools version using xcode-select -p
.class AndroidStudioDiagnostic(private val system: System) : Diagnostic() { override val title = "Android Studio" override fun diagnose(): Diagnosis { ... } private fun findAndroidStudio(path: String): Application? { Logger.d("findAndroidStudio($path)") val plist = system.parsePlist("$path/Contents/Info.plist") ?: return null val version = plist["CFBundleVersion"]?.toString()?.trim('"') ?: return null val name = plist["CFBundleName"]?.toString()?.trim('"') ?: path.substringAfterLast("/").substringBeforeLast(".") return Application(name, Version(version), path) } }
The
findAndroidStudio
method is a helper method used to find Android Studio installations on the system.The
diagnose
method finds the path to Android Studio installation(s) by searching various directories. It then iterates over each installation, checks whether the Kotlin plugin and Kotlin Multiplatform Mobile plugin are installed and enabled, and reports any failures, warnings or successes accordingly.The method also provides some additional information about the installed Android Studio versions, including the bundled Java version and the location of the Gradle JDK.
If multiple Android Studio installations are found, the method reports this as an info message. If no installations are found, the method reports a failure.
actual fun getSystem(): System = TODO("JVM target is only for unit tests")
This is a placeholder function for the
getSystem()
function which is used to create a System
instance that provides platform-dependent system operations like file I/O, process execution, and environment variables.The actual implementation of the
getSystem()
function for a specific platform (such as macOS) must be provided in order to use the diagnostic tool on that platform.