KDoctor: Your Ultimate Troubleshooting Companion for the Kotlin Multiplatform Ecosystem

Tools|Apr 19, 2023|Last edited: May 4, 2023
  • Tool
  • Kotlin
  • Compose Multiplatform
  • Kotlin Multiplatform
  • KDoctor: Your Ultimate Troubleshooting Companion for the Kotlin Multiplatform Ecosystem
    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:
    1. Install KDoctor.
    1. Open a terminal window.
    1. Type kdoctor and press Enter.
    1. Wait for KDoctor to complete the diagnostics. The output will show whether your system is ready for Kotlin Multiplatform Mobile development.
     
    Example output of KDoctor
    Example output of KDoctor
    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 in AndroidStudioDiagnostic class, which searches for Android Studio installations and filters out backups created by the JetBrains Toolbox.
    • findGradleWrapper() method in GradleDiagnostic 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 the JAVA_HOME environment variable is set correctly.
    • 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 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.
     
    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

     
    Clikt - command line library:
    clikt
    ajaltUpdated 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.
     
    Create a Stunning Animated Navigation Bar in Jetpack ComposeReveal: A Jetpack Compose Library for Engaging Reveal Effects