diff --git a/LLDBPlugin/touchlab_kotlin_lldb/types/KonanStringSyntheticProvider.py b/LLDBPlugin/touchlab_kotlin_lldb/types/KonanStringSyntheticProvider.py index 027e564..fe883e6 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/types/KonanStringSyntheticProvider.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/types/KonanStringSyntheticProvider.py @@ -26,4 +26,7 @@ def get_child_at_index(self, _): def to_string(self): s = kotlin_object_to_string(self._process, self._valobj.unsigned) - return '"{}"'.format(s) if s else self._valobj.GetValue() + if s is None: + return self._valobj.GetValue() + else: + return '"{}"'.format(s) diff --git a/build.gradle.kts b/build.gradle.kts index 06a11b2..409cac1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "co.touchlab" -version = "2.1.0" +version = "2.2.1" kotlin { listOf(macosX64(), macosArm64()).forEach { @@ -31,9 +31,9 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(libs.kotlinx.cli) + implementation(libs.clikt) + implementation(libs.mordant) implementation(libs.kotlinx.serialization.json) - implementation(libs.kermit) implementation(libs.kotlinx.coroutines.core) } } @@ -63,7 +63,6 @@ kotlin { } all { - languageSettings.optIn("kotlinx.cli.ExperimentalCli") languageSettings.optIn("kotlinx.cinterop.BetaInteropApi") languageSettings.optIn("kotlin.experimental.ExperimentalNativeApi") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a76a601..d9df730 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,18 @@ [versions] -kotlin = "2.0.0" +kotlin = "2.1.10" kotlinx-coroutines = "1.7.3" -kotlinx-cli = "0.3.6" kotlinx-serialization = "1.6.0" kermit = "1.2.2" gradle-doctor = "0.9.2" +clikt = "5.0.3" +mordant = "3.0.1" [libraries] kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinx-cli" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } +clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } +mordant = { module = "com.github.ajalt.mordant:mordant", version.ref = "mordant" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586..e382118 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/EchoWriter.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/EchoWriter.kt deleted file mode 100644 index d04aca5..0000000 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/EchoWriter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package co.touchlab.xcode.cli - -import co.touchlab.kermit.LogWriter -import co.touchlab.kermit.Severity -import kotlinx.cinterop.ExperimentalForeignApi -import platform.posix.fflush -import platform.posix.fprintf -import platform.posix.stderr - -class EchoWriter: LogWriter() { - override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - val printString: (String) -> Unit = when (severity) { - Severity.Verbose, Severity.Debug -> return - Severity.Info -> { string -> println(string) } - Severity.Warn -> { string -> println("WARN: $string") } - Severity.Error, Severity.Assert -> @OptIn(ExperimentalForeignApi::class) { string -> - fprintf(stderr, string) - fflush(stderr) - } - } - - printString(message) - throwable?.let { - printString(it.stackTraceToString()) - } - } -} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/InstallationFacade.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/InstallationFacade.kt index c5aad5b..f3d1c61 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/InstallationFacade.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/InstallationFacade.kt @@ -1,30 +1,26 @@ package co.touchlab.xcode.cli -import co.touchlab.kermit.Logger -import co.touchlab.xcode.cli.command.Install import co.touchlab.xcode.cli.util.Console object InstallationFacade { - private val logger = Logger.withTag("InstallationFacade") - - fun installAll(xcodeInstallations: List, fixXcode15: Boolean) { + suspend fun installAll(xcodeInstallations: List, fixXcode15: Boolean) { XcodeHelper.ensureXcodeNotRunning() val bundledVersion = PluginManager.bundledVersion - logger.v { "Bundled plugin version = $bundledVersion" } + Console.muted("Bundled plugin version = $bundledVersion") val installedVersion = PluginManager.installedVersion - logger.v { "Installed plugin version = ${installedVersion ?: "N/A"}" } + Console.muted("Installed plugin version = ${installedVersion ?: "N/A"}") if (installedVersion != null) { val (confirmation, notification) = when { bundledVersion > installedVersion -> { - "Do you want to update from $installedVersion to $bundledVersion? y/n: " to "Updating to $bundledVersion" + "Do you want to update from $installedVersion to $bundledVersion?" to "Updating to $bundledVersion" } bundledVersion == installedVersion -> { - "Do you want to reinstall version $installedVersion? y/n: " to "Reinstalling $installedVersion" + "Do you want to reinstall version $installedVersion?" to "Reinstalling $installedVersion" } bundledVersion < installedVersion -> { - "Do you want to downgrade from $installedVersion to $bundledVersion? y/n: " to "Downgrading to $bundledVersion" + "Do you want to downgrade from $installedVersion to $bundledVersion?" to "Downgrading to $bundledVersion" } else -> error("Unhandled comparison possibility!") } @@ -33,11 +29,11 @@ object InstallationFacade { return } - logger.v { "Installation confirmed." } - logger.i { notification } + Console.muted("Installation confirmed.") + Console.info(notification) uninstallAll() } else { - logger.i { "Installing $bundledVersion." } + Console.info("Installing $bundledVersion.") } PluginManager.install() @@ -50,36 +46,36 @@ object InstallationFacade { LLDBInitManager.install() PluginManager.enable(bundledVersion, xcodeInstallations) - logger.i { "Installation complete." } + Console.info("Installation complete.") } - fun enable(xcodeInstallations: List) { + suspend fun enable(xcodeInstallations: List) { XcodeHelper.ensureXcodeNotRunning() val installedVersion = PluginManager.installedVersion ?: run { - Console.echo("Plugin not installed, nothing to enable.") + Console.warning("Plugin not installed, nothing to enable.") return } PluginManager.enable(installedVersion, xcodeInstallations) - logger.i { "Plugin enabled." } + Console.info("Plugin enabled.") } - fun disable(xcodeInstallations: List) { + suspend fun disable(xcodeInstallations: List) { XcodeHelper.ensureXcodeNotRunning() val installedVersion = PluginManager.installedVersion ?: run { - Console.echo("Plugin not installed, nothing to disable.") + Console.warning("Plugin not installed, nothing to disable.") return } PluginManager.disable(installedVersion, xcodeInstallations) - logger.i { "Plugin disabled." } + Console.info("Plugin disabled.") } - fun fixXcode15(xcodeInstallations: List) { + suspend fun fixXcode15(xcodeInstallations: List) { XcodeHelper.ensureXcodeNotRunning() val installedVersion = PluginManager.installedVersion @@ -95,14 +91,14 @@ object InstallationFacade { } } - logger.i { "Xcode 15 fix applied." } + Console.info("Xcode 15 fix applied.") } - fun sync(xcodeInstallations: List, fixXcode15: Boolean) { + suspend fun sync(xcodeInstallations: List, fixXcode15: Boolean) { XcodeHelper.ensureXcodeNotRunning() val installedVersion = PluginManager.installedVersion ?: run { - Console.echo("Plugin not installed, nothing to synchronize.") + Console.warning("Plugin not installed, nothing to synchronize.") return } @@ -113,16 +109,16 @@ object InstallationFacade { } PluginManager.enable(installedVersion, xcodeInstallations) - logger.i { "Synchronization complete." } + Console.info("Synchronization complete.") } - fun uninstallAll() { - logger.v { "Will uninstall all plugin components." } + suspend fun uninstallAll() { + Console.muted("Will uninstall all plugin components.") XcodeHelper.ensureXcodeNotRunning() PluginManager.uninstall() LangSpecManager.uninstall() LLDBInitManager.uninstall() - logger.i { "Uninstallation complete." } + Console.info("Uninstallation complete.") } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/LLDBInitManager.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/LLDBInitManager.kt index a925f20..23062d7 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/LLDBInitManager.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/LLDBInitManager.kt @@ -1,6 +1,5 @@ package co.touchlab.xcode.cli -import co.touchlab.kermit.Logger import co.touchlab.xcode.cli.util.Console import co.touchlab.xcode.cli.util.File import co.touchlab.xcode.cli.util.Path @@ -15,7 +14,6 @@ object LLDBInitManager { private val sourceMainLlvmInit = "command source ~/.lldbinit" private val lldbInitFile = File(Path.home / ".lldbinit") private val lldbInitXcodeFile = File(Path.home / ".lldbinit-Xcode") - private val logger = Logger.withTag("LLDBInitManager") val isInstalled: Boolean get() { @@ -38,7 +36,7 @@ object LLDBInitManager { val oldContents = when { lldbInitXcodeFile.exists() -> { - logger.v { "${lldbInitXcodeFile.path} exists, will append LLDB init into it." } + Console.muted("${lldbInitXcodeFile.path} exists, will append LLDB init into it.") lldbInitXcodeFile.stringContents().kt } lldbInitFile.exists() -> { @@ -46,8 +44,8 @@ object LLDBInitManager { This installer will create a new file at '~/.lldbinit-Xcode'. This file takes precedence over the '~/.lldbinit' file. To keep using configuration from '~/.lldbinit' file, it needs to be sourced by the newly created '~/.lldbinit-Xcode' file. """.trimIndent()) - if (Console.confirm("Do you want to source the '~/.lldbinit' file? y/n: ")) { - logger.v { "Will source ~/.lldbinit." } + if (Console.confirm("Do you want to source the '~/.lldbinit' file?")) { + Console.muted("Will source ~/.lldbinit.") sourceMainLlvmInit } else { "" @@ -60,13 +58,13 @@ object LLDBInitManager { val oldAndNewContentsSeparator = if (!oldContents.endsWith("\n")) "\n" else "" val newContents = oldContents + oldAndNewContentsSeparator + sourcePluginInit - logger.v { "Saving new LLDB init to ${lldbInitXcodeFile.path}." } + Console.muted("Saving new LLDB init to ${lldbInitXcodeFile.path}.") lldbInitXcodeFile.write(newContents.objc) } fun uninstall() { if (isInstalled) { - logger.v { "LLDB init script found, removing." } + Console.muted("LLDB init script found, removing.") val oldContents = lldbInitXcodeFile.stringContents() val newContents = oldContents.stringByReplacingOccurrencesOfString(target = sourcePluginInit, withString = "") lldbInitXcodeFile.write(newContents.objc) @@ -75,7 +73,6 @@ object LLDBInitManager { } object Legacy { - private val logger = Logger.withTag("LLDBInitManager") private val legacyImport = """ command script import ~/Library/Developer/Xcode/Plug-ins/Kotlin.ideplugin/Contents/Resources/konan_lldb_config.py command script import ~/Library/Developer/Xcode/Plug-ins/Kotlin.ideplugin/Contents/Resources/konan_lldb.py @@ -91,11 +88,11 @@ object LLDBInitManager { fun uninstall() { if (isInstalled) { - logger.v { "Legacy LLDB script initialization found, removing." } + Console.muted("Legacy LLDB script initialization found, removing.") val oldContents = lldbInitXcodeFile.stringContents() val newContents = oldContents.stringByReplacingOccurrencesOfString(target = legacyImport, withString = "") lldbInitXcodeFile.write(newContents.objc) } } } -} \ No newline at end of file +} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/LangSpecManager.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/LangSpecManager.kt index c2df703..18a4889 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/LangSpecManager.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/LangSpecManager.kt @@ -1,6 +1,6 @@ package co.touchlab.xcode.cli -import co.touchlab.kermit.Logger +import co.touchlab.xcode.cli.util.Console import co.touchlab.xcode.cli.util.File import co.touchlab.xcode.cli.util.Path @@ -9,21 +9,20 @@ object LangSpecManager { private val specSourceFile = File(Path.dataDir / specName) private val specsDirectory = File(XcodeHelper.xcodeLibraryPath / "Specifications") private val specTargetFile = File(specsDirectory.path / specName) - private val logger = Logger.withTag("LangSpecManager") val isInstalled: Boolean get() = specTargetFile.exists() fun install() { check(!specTargetFile.exists()) { "Language spec file exists at path ${specTargetFile.path}! Delete it first." } - logger.v { "Ensuring language specification directory exists at ${specsDirectory.path}" } + Console.muted("Ensuring language specification directory exists at ${specsDirectory.path}") specsDirectory.mkdirs() - logger.v { "Copying language specification to target path ${specTargetFile.path}" } + Console.muted("Copying language specification to target path ${specTargetFile.path}") specSourceFile.copy(specTargetFile.path) } fun uninstall() { - logger.v { "Deleting language specification from ${specTargetFile.path}." } + Console.muted("Deleting language specification from ${specTargetFile.path}.") specTargetFile.delete() } -} \ No newline at end of file +} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/PluginManager.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/PluginManager.kt index 0e76ac0..e72fbc7 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/PluginManager.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/PluginManager.kt @@ -1,8 +1,19 @@ package co.touchlab.xcode.cli -import co.touchlab.kermit.Logger import co.touchlab.xcode.cli.util.* -import platform.posix.sleep +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import platform.posix.nice +import kotlin.coroutines.coroutineContext +import kotlin.time.Duration.Companion.seconds object PluginManager { val pluginName = "Kotlin.ideplugin" @@ -14,7 +25,6 @@ object PluginManager { private val pluginTargetInfoFile = File(pluginTargetFile.path / "Contents" / "Info.plist") private val pluginVersionInfoKey = "CFBundleShortVersionString" private val pluginCompatibilityInfoKey = "DVTPlugInCompatibilityUUIDs" - private val logger = Logger.withTag("PluginManager") private val fixXcode15Timeout = 10 val bundledVersion: SemVer @@ -48,23 +58,23 @@ object PluginManager { } fun install() { - logger.v { "Ensuring plugins directory exists at ${pluginsDirectory.path}" } + Console.muted("Ensuring plugins directory exists at ${pluginsDirectory.path}") pluginsDirectory.mkdirs() - logger.v { "Copying Xcode plugin to target path ${pluginTargetFile.path}" } + Console.muted("Copying Xcode plugin to target path ${pluginTargetFile.path}") pluginSourceFile.copy(pluginTargetFile.path) } - fun enable(version: SemVer, xcodeInstallations: List) { - logger.i { "Removing Kotlin Plugin defaults so we can add it to allowed." } + suspend fun enable(version: SemVer, xcodeInstallations: List) { + Console.info("Removing Kotlin Plugin defaults so we can add it to allowed.") XcodeHelper.removeKotlinPluginFromDefaults() - logger.i { "Allowing Kotlin Plugin" } + Console.info("Allowing Kotlin Plugin") XcodeHelper.allowKotlinPlugin(version, xcodeInstallations) } - - fun disable(version: SemVer, xcodeInstallations: List) { - logger.i { "Removing Kotlin Plugin defaults so we can add it to skipped." } + suspend fun disable(version: SemVer, xcodeInstallations: List) { + nice(420) + Console.info("Removing Kotlin Plugin defaults so we can add it to skipped.") XcodeHelper.removeKotlinPluginFromDefaults() - logger.i { "We need Xcode to skip the plugin, so it doesn't crash." } + Console.info("We need Xcode to skip the plugin, so it doesn't crash.") XcodeHelper.skipKotlinPlugin(version, xcodeInstallations) } @@ -74,55 +84,57 @@ object PluginManager { Console.echo("Synchronizing plugin compatibility list.") val additionalPluginCompatibilityIds = xcodeInstallations.mapNotNull { it.pluginCompatabilityId?.let { PropertyList.Object.String(it) } } - logger.v { "Xcode installation IDs to include: ${additionalPluginCompatibilityIds.joinToString { it.value }}" } + Console.muted("Xcode installation IDs to include: ${additionalPluginCompatibilityIds.joinToString { it.value }}") val infoPlist = PropertyList.create(pluginTargetInfoFile) val rootDictionary = infoPlist.root.dictionary val oldPluginCompatibilityIds = rootDictionary .getOrPut(pluginCompatibilityInfoKey) { PropertyList.Object.Array(mutableListOf()) } .array - logger.v { + Console.muted( "Previous Xcode installation IDs: ${ oldPluginCompatibilityIds.mapNotNull { it.stringOrNull?.value }.joinToString() }" - } + ) oldPluginCompatibilityIds.addAll(additionalPluginCompatibilityIds) val distinctPluginCompatibilityIds = oldPluginCompatibilityIds.distinctBy { it.stringOrNull?.value }.toMutableList() - logger.v { + Console.muted( "Xcode installation IDs to save: ${ distinctPluginCompatibilityIds.mapNotNull { it.stringOrNull?.value }.joinToString() }" - } + ) rootDictionary[pluginCompatibilityInfoKey] = PropertyList.Object.Array(distinctPluginCompatibilityIds) pluginTargetInfoFile.write(infoPlist.toData(PropertyList.Format.XML)) } - fun uninstall() { - logger.i { "Deleting Xcode plugin from ${pluginTargetFile.path}" } + suspend fun uninstall() { + Console.info("Deleting Xcode plugin from ${pluginTargetFile.path}") pluginTargetFile.delete() XcodeHelper.removeKotlinPluginFromDefaults() } - fun fixXcode15(xcodeInstallations: List): Unit = try { + suspend fun fixXcode15(xcodeInstallations: List): Unit = try { val cacheDir = Path(Shell.exec("/usr/bin/getconf", "DARWIN_USER_CACHE_DIR").output.orEmpty().trim()) - logger.i { "Enabling IDEPerformanceDebugger built-in plugin." } + Console.info("Enabling IDEPerformanceDebugger built-in plugin.") XcodeHelper.setIDEPerformanceDebuggerEnabled(true) xcodeInstallations .filter { it.version.startsWith("15.") } .forEach { installation -> - logger.i { "Opening ${installation.name} in background to generate plugin cache" } - XcodeHelper.openInBackground(installation) + Console.info("Opening ${installation.name} in background to generate plugin cache") + val xcodeRunning = CoroutineScope(coroutineContext + Dispatchers.IO).launch { + XcodeHelper.openInBackground(installation) + } try { for (i in 1..fixXcode15Timeout) { - sleep(1u) + delay(1.seconds) val pluginCachePath = cacheDir / "com.apple.DeveloperTools" / "${installation.version}-${installation.build}" / "Xcode" / "PlugInCache-Debug.xcplugincache" if (pluginCachePath.exists()) { - logger.i { "${installation.name} plugin cache file exists, checking if it contains IDEPerformanceDebugger entry yet" } + Console.info("${installation.name} plugin cache file exists, checking if it contains IDEPerformanceDebugger entry yet") val pluginCache = PropertyList.create(pluginCachePath) val containsIDEPerformanceDebuggerInfo = with(XcodeHelper.PlugInCache) { pluginCache.scanRecords.contains { record -> @@ -132,13 +144,13 @@ object PluginManager { if (containsIDEPerformanceDebuggerInfo) { // Xcode updated the cache and should work now, we're done. - logger.i { "${installation.name} updated the plugin cache and should work now." } + Console.info("${installation.name} updated the plugin cache and should work now.") break } else { - logger.i { "${installation.name} plugin cache doesn't contain IDEPerformanceDebugger entry yet." } + Console.info("${installation.name} plugin cache doesn't contain IDEPerformanceDebugger entry yet.") } } else { - logger.i { "${installation.name} plugin cache file doesn't exist yet." } + Console.info("${installation.name} plugin cache file doesn't exist yet.") } if (i == fixXcode15Timeout) { @@ -146,14 +158,12 @@ object PluginManager { } } } finally { - logger.i { "Killing ${installation.name}" } - XcodeHelper.killRunningXcode().checkSuccessful { - "Couldn't shut down Xcode!" - } + Console.info("Killing ${installation.name}") + xcodeRunning.cancelAndJoin() } } } finally { - logger.i { "Disabling IDEPerformanceDebugger built-in plugin." } + Console.info("Disabling IDEPerformanceDebugger built-in plugin.") XcodeHelper.setIDEPerformanceDebuggerEnabled(false) } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/XcodeHelper.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/XcodeHelper.kt index 6649ab8..85ab65d 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/XcodeHelper.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/XcodeHelper.kt @@ -1,6 +1,5 @@ package co.touchlab.xcode.cli -import co.touchlab.kermit.Logger import co.touchlab.xcode.cli.util.BackupHelper import co.touchlab.xcode.cli.util.Console import co.touchlab.xcode.cli.util.File @@ -18,45 +17,48 @@ import platform.posix.exit object XcodeHelper { val xcodeLibraryPath = Path.home / "Library" / "Developer" / "Xcode" - private val logger = Logger.withTag("XcodeHelper") private val xcodeProcessName = "Xcode" private val xcodeVersionRegex = Regex("(\\S+) \\((\\S+)\\)") - fun ensureXcodeNotRunning() { - logger.v { "Checking if any Xcode runs." } + suspend fun ensureXcodeNotRunning() { + Console.muted("Checking if any Xcode runs.") val result = Shell.exec("/usr/bin/pgrep", "-xq", "--", xcodeProcessName) if (result.success) { - logger.v { "Found running Xcode instance." } - val shutdown = Console.confirm("Xcode is running. Attempt to shut down? y/n: ") + Console.muted("Found running Xcode instance.") + val shutdown = Console.confirm("Xcode is running. Attempt to shut down?") if (shutdown) { - logger.v { "Shutting down Xcode." } + Console.muted("Shutting down Xcode.") Console.echo("Shutting down Xcode...") killRunningXcode().checkSuccessful { "Couldn't shut down Xcode!" } } else { - Console.printError("Xcode needs to be closed!") + Console.danger("Xcode needs to be closed!") exit(1) } } else { - logger.v { "No running Xcode found." } + Console.muted("No running Xcode found.") } } - fun openInBackground(installation: XcodeInstallation) { - Shell.exec("/usr/bin/open", "-gjFa", installation.path.value).checkSuccessful { - "Couldn't open ${installation.name} at ${installation.path}!" + suspend fun openInBackground(installation: XcodeInstallation) { + val xcodeBinaryPath = installation.path / "Contents" / "MacOS" / "Xcode" + Console.info("Opening Xcode binary in background ($xcodeBinaryPath).") +// Console.echo("Opening realXcodePath in background.") + +// Shell.exec("/usr/bin/open", "-gjF", xcodeBinaryPath.value) + Shell.exec(xcodeBinaryPath.value).checkSuccessful { + "Couldn't open ${installation.name} at $xcodeBinaryPath!" } } - fun killRunningXcode(): Shell.ExecutionResult { + suspend fun killRunningXcode(): Shell.ExecutionResult { return Shell.exec("/usr/bin/pkill", "-x", xcodeProcessName) } - fun allXcodeInstallations(): List { - val result = Shell.exec("/usr/sbin/system_profiler", "-json", "SPDeveloperToolsDataType").checkSuccessful { - "Couldn't get list of installed developer tools." - } + suspend fun allXcodeInstallations(): List { + val result = Shell.exec("/usr/sbin/system_profiler", "-json", "SPDeveloperToolsDataType") + .checkSuccessful { "Couldn't get list of installed developer tools." } val json = Json { ignoreUnknownKeys = true @@ -72,7 +74,7 @@ object XcodeHelper { } } - fun installationAt(path: Path): XcodeInstallation { + suspend fun installationAt(path: Path): XcodeInstallation { val xcodeFile = File(path) require(xcodeFile.exists()) { "Path $path doesn't exist!" } val versionPlist = PropertyList.create(path / "Contents" / "version.plist") @@ -108,8 +110,8 @@ object XcodeHelper { ) } - fun allowKotlinPlugin(pluginVersion: SemVer, xcodeInstallations: List) { - logger.i { "Adding plugin to allowed list in Xcode defaults." } + suspend fun allowKotlinPlugin(pluginVersion: SemVer, xcodeInstallations: List) { + Console.info("Adding plugin to allowed list in Xcode defaults.") modifyingXcodeDefaults("BeforeAdd") { for (installation in xcodeInstallations) { it.nonApplePlugins(installation.version).allowed.add(xcodeKotlinBundleId, pluginVersion.toString()) @@ -117,8 +119,8 @@ object XcodeHelper { } } - fun skipKotlinPlugin(pluginVersion: SemVer, xcodeInstallations: List) { - logger.i { "Adding plugin to skipped list in Xcode defaults." } + suspend fun skipKotlinPlugin(pluginVersion: SemVer, xcodeInstallations: List) { + Console.info("Adding plugin to skipped list in Xcode defaults.") modifyingXcodeDefaults("BeforeSkip") { for (installation in xcodeInstallations) { it.nonApplePlugins(installation.version).skipped.add(xcodeKotlinBundleId, pluginVersion.toString()) @@ -126,8 +128,8 @@ object XcodeHelper { } } - fun removeKotlinPluginFromDefaults() { - logger.i { "Removing plugin from allowed/skipped list in Xcode defaults." } + suspend fun removeKotlinPluginFromDefaults() { + Console.info("Removing plugin from allowed/skipped list in Xcode defaults.") modifyingXcodeDefaults("BeforeRemove") { properties -> properties.allNonApplePlugins().forEach { it.allowed.remove(xcodeKotlinBundleId) @@ -136,8 +138,8 @@ object XcodeHelper { } } - fun setIDEPerformanceDebuggerEnabled(enabled: Boolean) { - logger.i { "Setting IDEPerformanceDebuggerEnabled to $enabled in Xcode defaults." } + suspend fun setIDEPerformanceDebuggerEnabled(enabled: Boolean) { + Console.info("Setting IDEPerformanceDebuggerEnabled to $enabled in Xcode defaults.") modifyingXcodeDefaults("BeforeIDEPerformanceDebuggerEnabled-$enabled") { it.root.dictionary["IDEPerformanceDebuggerEnabled"] = PropertyList.Object.Number( NSNumber.numberWithBool(enabled) @@ -145,9 +147,9 @@ object XcodeHelper { } } - private inline fun modifyingXcodeDefaults(backupTag: String, modify: Defaults.(PropertyList) -> Unit) { + private suspend inline fun modifyingXcodeDefaults(backupTag: String, modify: Defaults.(PropertyList) -> Unit) { val backupPath = BackupHelper.backupPath("XcodeDefaults_$backupTag.plist") - logger.i { "Saving a backup of com.apple.dt.Xcode defaults to `$backupPath`" } + Console.info("Saving a backup of com.apple.dt.Xcode defaults to `$backupPath`") Shell.exec("/usr/bin/defaults", "export", "com.apple.dt.Xcode", backupPath.value).checkSuccessful { "Couldn't export Xcode defaults." } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/BaseXcodeListSubcommand.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/BaseXcodeListSubcommand.kt index 831e492..cf8b043 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/BaseXcodeListSubcommand.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/BaseXcodeListSubcommand.kt @@ -2,21 +2,29 @@ package co.touchlab.xcode.cli.command import co.touchlab.xcode.cli.XcodeHelper import co.touchlab.xcode.cli.util.Path -import kotlinx.cli.ArgType -import kotlinx.cli.Subcommand -import kotlinx.cli.default -import kotlinx.cli.optional -import kotlinx.cli.vararg +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option -abstract class BaseXcodeListSubcommand(name: String, actionDescription: String): Subcommand(name, actionDescription) { + +abstract class BaseXcodeListSubcommand( + name: String, + private val actionDescription: String, +): SuspendingCliktCommand(name) { protected val onlyProvidedXcodeInstallations by option( - type = ArgType.Boolean, - fullName = "only", - description = "Do not auto-discover Xcode installations, use only those provided." - ).default(false) - protected val providedXcodePaths by argument(type = ArgType.String, description = "").vararg().optional() + "--only", + help = "Do not auto-discover Xcode installations, use only those provided.", + ).flag(default = false, defaultForHelp = "disabled") + protected val providedXcodePaths by argument().multiple() + + override fun help(context: Context): String { + return actionDescription + } - protected fun xcodeInstallations(): List { + protected suspend fun xcodeInstallations(): List { val providedXcodeInstallations = providedXcodePaths.map { XcodeHelper.installationAt(Path(it)) } return if (onlyProvidedXcodeInstallations) { providedXcodeInstallations @@ -24,4 +32,4 @@ abstract class BaseXcodeListSubcommand(name: String, actionDescription: String): XcodeHelper.allXcodeInstallations() + providedXcodeInstallations } } -} \ No newline at end of file +} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Disable.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Disable.kt index 2f6d9d6..de54ac4 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Disable.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Disable.kt @@ -3,7 +3,7 @@ package co.touchlab.xcode.cli.command import co.touchlab.xcode.cli.InstallationFacade class Disable: BaseXcodeListSubcommand("disable", "Disables Xcode Kotlin plugin without uninstalling") { - override fun execute() { + override suspend fun run() { InstallationFacade.disable(xcodeInstallations()) } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Enable.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Enable.kt index e4e2870..3d85e30 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Enable.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Enable.kt @@ -3,7 +3,7 @@ package co.touchlab.xcode.cli.command import co.touchlab.xcode.cli.InstallationFacade class Enable: BaseXcodeListSubcommand("enable", "Enables Xcode Kotlin plugin") { - override fun execute() { + override suspend fun run() { InstallationFacade.enable(xcodeInstallations()) } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/FixXcode15.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/FixXcode15.kt index 842a629..b2d5b46 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/FixXcode15.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/FixXcode15.kt @@ -6,7 +6,7 @@ class FixXcode15: BaseXcodeListSubcommand( name = "fix-xcode15", actionDescription = "Temporarily workarounds Xcode 15 crash that happens when using any non-Apple Xcode plugins.", ) { - override fun execute() { + override suspend fun run() { InstallationFacade.fixXcode15(xcodeInstallations()) } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Info.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Info.kt index d82bbd6..f520509 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Info.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Info.kt @@ -7,7 +7,7 @@ import co.touchlab.xcode.cli.util.Console class Info: BaseXcodeListSubcommand("info", "Shows information about the plugin") { - override fun execute() = with(Console) { + override suspend fun run() = with(Console) { val installedVersion = PluginManager.installedVersion val bundledVersion = PluginManager.bundledVersion @@ -57,4 +57,4 @@ class Info: BaseXcodeListSubcommand("info", "Shows information about the plugin" private val Boolean.humanReadable: String get() = if (this) "Yes" else "No" -} \ No newline at end of file +} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Install.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Install.kt index 09333df..e5331d6 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Install.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Install.kt @@ -1,20 +1,16 @@ package co.touchlab.xcode.cli.command -import co.touchlab.kermit.Logger import co.touchlab.xcode.cli.InstallationFacade -import co.touchlab.xcode.cli.PluginManager -import co.touchlab.xcode.cli.util.Console -import kotlinx.cli.ArgType -import kotlinx.cli.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option class Install: BaseXcodeListSubcommand("install", "Installs Xcode Kotlin plugin") { private val noFixXcode15 by option( - type = ArgType.Boolean, - fullName = "no-fix-xcode15", - description = "Do not apply Xcode 15 workaround." - ).default(false) + "--no-fix-xcode15", + help = "Do not apply Xcode 15 workaround.", + ).flag(default = false) - override fun execute() { + override suspend fun run() { InstallationFacade.installAll(xcodeInstallations(), !noFixXcode15) } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Sync.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Sync.kt index e87cfcd..091172b 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Sync.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Sync.kt @@ -1,19 +1,16 @@ package co.touchlab.xcode.cli.command import co.touchlab.xcode.cli.InstallationFacade -import co.touchlab.xcode.cli.PluginManager -import co.touchlab.xcode.cli.XcodeHelper -import kotlinx.cli.ArgType -import kotlinx.cli.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option class Sync: BaseXcodeListSubcommand("sync", "Adds IDs of Xcode installations to the currently installed Xcode Kotlin plugin") { private val noFixXcode15 by option( - type = ArgType.Boolean, - fullName = "no-fix-xcode15", - description = "Do not apply Xcode 15 workaround." - ).default(false) + "--no-fix-xcode15", + help = "Do not apply Xcode 15 workaround." + ).flag() - override fun execute() { + override suspend fun run() { InstallationFacade.sync(xcodeInstallations(), !noFixXcode15) } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Uninstall.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Uninstall.kt index c93ccbf..00f402d 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Uninstall.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/Uninstall.kt @@ -1,10 +1,15 @@ package co.touchlab.xcode.cli.command import co.touchlab.xcode.cli.InstallationFacade -import kotlinx.cli.Subcommand +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context -class Uninstall: Subcommand("uninstall", "Uninstalls Xcode Kotlin plugin") { - override fun execute() { +class Uninstall: SuspendingCliktCommand("uninstall") { + override fun help(context: Context): String { + return "Uninstalls Xcode Kotlin plugin" + } + + override suspend fun run() { InstallationFacade.uninstallAll() } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/command/XcodeKotlinPlugin.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/XcodeKotlinPlugin.kt new file mode 100644 index 0000000..d20e1eb --- /dev/null +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/command/XcodeKotlinPlugin.kt @@ -0,0 +1,13 @@ +package co.touchlab.xcode.cli.command + +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.terminal +import com.github.ajalt.mordant.terminal.muted + +class XcodeKotlinPlugin( + private val args: Array, +): SuspendingCliktCommand() { + override suspend fun run() { + terminal.muted("Running xcode-cli with arguments: ${args.joinToString()}") + } +} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/main.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/main.kt index b536514..3c3b93b 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/main.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/main.kt @@ -1,54 +1,52 @@ package co.touchlab.xcode.cli -import co.touchlab.kermit.CommonWriter -import co.touchlab.kermit.Logger -import co.touchlab.kermit.platformLogWriter -import co.touchlab.xcode.cli.command.* +import co.touchlab.xcode.cli.command.Disable +import co.touchlab.xcode.cli.command.Enable +import co.touchlab.xcode.cli.command.FixXcode15 +import co.touchlab.xcode.cli.command.Info +import co.touchlab.xcode.cli.command.Install +import co.touchlab.xcode.cli.command.Sync +import co.touchlab.xcode.cli.command.Uninstall +import co.touchlab.xcode.cli.command.XcodeKotlinPlugin import co.touchlab.xcode.cli.util.Console import co.touchlab.xcode.cli.util.CrashHelper -import kotlinx.cli.ArgParser +import com.github.ajalt.clikt.command.main +import com.github.ajalt.clikt.core.context +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.core.terminal +import kotlinx.coroutines.runBlocking import platform.posix.exit fun main(args: Array) { val crashHelper = CrashHelper() try { - val logWriters = if (args.contains("--log-console")) { - listOf(platformLogWriter(), CommonWriter(), crashHelper) - } else { - listOf(platformLogWriter(), EchoWriter(), crashHelper) - } - - Logger.setLogWriters(logWriters) - Logger.v { "Running xcode-cli with arguments: ${args.joinToString()}" } + val command = XcodeKotlinPlugin(args) + .context { + this.terminal = Console.terminal + } + .subcommands( + Install(), + Uninstall(), + Sync(), + Info(), + FixXcode15(), + Enable(), + Disable(), + ) - // If no arguments were given, we want to show help. - val adjustedArgs = if (args.isEmpty()) { - arrayOf("-h") - } else { - args.filter { it != "--log-console" }.toTypedArray() + runBlocking { + command.main(args) } - val parser = ArgParser("xcode-kotlin") - parser.subcommands( - Install(), - Uninstall(), - Sync(), - Info(), - FixXcode15(), - Enable(), - Disable(), - ) - - parser.parse(adjustedArgs) } catch (e: Throwable) { - if (Console.confirm("xcode-kotlin has crashed, do you want to upload the crash report to Touchlab? y/n: ")) { + if (Console.confirm("xcode-kotlin has crashed, do you want to upload the crash report to Touchlab?")) { Console.echo("Uploading crash report.") try { - crashHelper.upload(e) - Console.echo("Upload successful") + crashHelper.upload(e, Console.terminalRecorder.recorder) + Console.success("Upload successful") exit(1) } catch (uploadException: Throwable) { - Console.printError("Uploading crash report failed!") + Console.danger("Uploading crash report failed!") e.addSuppressed(uploadException) throw e } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/BackupHelper.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/BackupHelper.kt index d0a2b6f..279e363 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/BackupHelper.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/BackupHelper.kt @@ -13,5 +13,4 @@ object BackupHelper { backupRoot.mkdirs() } } - -} \ No newline at end of file +} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Console.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Console.kt index a64aa60..4509f28 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Console.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Console.kt @@ -1,45 +1,50 @@ +@file:Suppress("invisible_reference", "invisible_member") + package co.touchlab.xcode.cli.util -import co.touchlab.kermit.Logger -import kotlinx.cinterop.ExperimentalForeignApi -import platform.posix.fflush -import platform.posix.fprintf -import platform.posix.stderr +import com.github.ajalt.mordant.internal.STANDARD_TERM_INTERFACE +import com.github.ajalt.mordant.terminal.Terminal +import com.github.ajalt.mordant.terminal.YesNoPrompt +import com.github.ajalt.mordant.terminal.danger +import com.github.ajalt.mordant.terminal.info +import com.github.ajalt.mordant.terminal.muted +import com.github.ajalt.mordant.terminal.success +import com.github.ajalt.mordant.terminal.warning object Console { - private val logger = Logger.withTag("Console") + val terminalRecorder = DelegatingTerminalRecorder(delegate = STANDARD_TERM_INTERFACE) + val terminal = Terminal(terminalInterface = terminalRecorder) fun echo(text: String = "") { - logger.v { text } - println(text) + terminal.println(text) } fun confirm(text: String): Boolean { while (true) { - val input = prompt(text, newLine = false)?.trim()?.lowercase() ?: return false - return when (input) { - "y", "yes" -> true - "n", "no" -> false - else -> { - printError("Invalid input '$input', try again.") - continue - } + val result = YesNoPrompt(text, terminal).ask() + if (result != null) { + return result } } } - fun prompt(text: String, newLine: Boolean = false): String? { - if (newLine) { - println(text) - } else { - print(text) - } - return readLine() + fun muted(message: String) { + terminal.muted(message) + } + + fun info(message: String) { + terminal.info(message) + } + + fun warning(message: String) { + terminal.warning(message) + } + + fun danger(message: String) { + terminal.danger(message, stderr = true) } - @OptIn(ExperimentalForeignApi::class) - fun printError(message: String) { - fprintf(stderr, message) - fflush(stderr) + fun success(message: String) { + terminal.success(message) } } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/CrashHelper.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/CrashHelper.kt index 29f70af..e4ee00b 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/CrashHelper.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/CrashHelper.kt @@ -1,21 +1,23 @@ package co.touchlab.xcode.cli.util -import co.touchlab.kermit.LogWriter -import co.touchlab.kermit.Severity +import com.github.ajalt.mordant.terminal.TerminalRecorder import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.runBlocking -import platform.Foundation.* +import platform.Foundation.NSError +import platform.Foundation.NSMutableURLRequest +import platform.Foundation.NSURL +import platform.Foundation.NSURLSession import platform.Foundation.NSURLSessionConfiguration.Companion.defaultSessionConfiguration +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.dataTaskWithRequest +import platform.Foundation.dataUsingEncoding +import platform.Foundation.setHTTPBody +import platform.Foundation.setHTTPMethod +import platform.Foundation.setValue -class CrashHelper : LogWriter() { - private val logEntries = mutableListOf() - - override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { - logEntries.add(LogEntry(severity, message, tag, throwable)) - } - - fun upload(e: Throwable) { - upload(capture(e)) +class CrashHelper { + fun upload(e: Throwable, recorder: TerminalRecorder) { + upload(capture(e, recorder.output())) } private fun upload(crashReport: String) { @@ -48,10 +50,10 @@ class CrashHelper : LogWriter() { } } - private fun capture(e: Throwable): String = buildString { - if (logEntries.isNotEmpty()) { + private fun capture(e: Throwable, recorderOutput: String): String = buildString { + if (recorderOutput.isNotBlank()) { append("BREADCRUMBS\n===========\n\n") - append(logEntries.joinToString("\n")) + append(recorderOutput) append("\n\n") } @@ -59,15 +61,5 @@ class CrashHelper : LogWriter() { append(e.stackTraceToString()) } - data class LogEntry(val severity: Severity, val message: String, val tag: String, val throwable: Throwable?) { - override fun toString(): String = buildString { - append("$severity - $tag - $message") - throwable?.let { - append("\n") - append(it.getStackTrace().joinToString("\n")) - } - } - } - class CrashReportUploadException(val error: NSError): Exception("Crash report upload failed: ${error.localizedDescription}") } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/DelegatingTerminalRecorder.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/DelegatingTerminalRecorder.kt new file mode 100644 index 0000000..3ce6cf6 --- /dev/null +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/DelegatingTerminalRecorder.kt @@ -0,0 +1,27 @@ +package co.touchlab.xcode.cli.util + +import com.github.ajalt.mordant.rendering.AnsiLevel +import com.github.ajalt.mordant.terminal.PrintRequest +import com.github.ajalt.mordant.terminal.TerminalInfo +import com.github.ajalt.mordant.terminal.TerminalInterface +import com.github.ajalt.mordant.terminal.TerminalRecorder + +class DelegatingTerminalRecorder( + val delegate: TerminalInterface, +): TerminalInterface { + val recorder = TerminalRecorder() + + override fun completePrintRequest(request: PrintRequest) { + delegate.completePrintRequest(request) + recorder.completePrintRequest(request) + } + + override fun info( + ansiLevel: AnsiLevel?, + hyperlinks: Boolean?, + outputInteractive: Boolean?, + inputInteractive: Boolean? + ): TerminalInfo = delegate.info(ansiLevel, hyperlinks, outputInteractive, inputInteractive) + + override fun readLineOrNull(hideInput: Boolean): String? = delegate.readLineOrNull(hideInput) +} diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/File.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/File.kt index 6c51b67..923c4bc 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/File.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/File.kt @@ -71,5 +71,5 @@ class File(private val providedPath: Path, private val resolveSymlinks: Boolean } } - class IOException(val nsError: NSError): Exception(nsError.description) + class IOException(nsError: NSError): Exception(nsError.description) } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/PropertyList.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/PropertyList.kt index 4a81e79..43a81d8 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/PropertyList.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/PropertyList.kt @@ -1,7 +1,11 @@ package co.touchlab.xcode.cli.util -import co.touchlab.kermit.Logger -import kotlinx.cinterop.* +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCObjectVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value import platform.Foundation.NSArray import platform.Foundation.NSData import platform.Foundation.NSDate @@ -17,9 +21,21 @@ import platform.Foundation.NSPropertyListOpenStepFormat import platform.Foundation.NSPropertyListSerialization import platform.Foundation.NSPropertyListXMLFormat_v1_0 import platform.Foundation.NSString -import platform.Foundation.create import platform.Foundation.valueForKey import platform.darwin.NSObject +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.MutableList +import kotlin.collections.MutableMap +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.iterator +import kotlin.collections.map +import kotlin.collections.mutableListOf +import kotlin.collections.mutableMapOf +import kotlin.collections.set +import kotlin.collections.toMutableList +import kotlin.collections.toMutableMap @OptIn(ExperimentalForeignApi::class) class PropertyList(val root: Object) { @@ -110,20 +126,18 @@ class PropertyList(val root: Object) { is Object.String -> obj.value.objc } - class UnsupportedObjectTypeException(val nsObject: NSObject): Exception("Unsupported property list value: $nsObject") - class DeserializationException(val error: NSError): Exception("Could not deserialize property list. Error: ${error.description}.") - class SerializationException(val error: NSError): Exception("Could not serialize property list. Error: ${error.description}.") + class UnsupportedObjectTypeException(nsObject: NSObject): Exception("Unsupported property list value: $nsObject") + class DeserializationException(error: NSError): Exception("Could not deserialize property list. Error: ${error.description}.") + class SerializationException(error: NSError): Exception("Could not serialize property list. Error: ${error.description}.") companion object { - private val logger = Logger.withTag("PropertyList") - fun create(path: Path): PropertyList { - logger.v { "Loading property list from $path." } + Console.muted("Loading property list from $path.") return create(File(path).dataContents()) } fun create(file: File): PropertyList { - logger.v { "Loading property list from file at ${file.path}" } + Console.muted("Loading property list from file at ${file.path}") return create(file.dataContents()) } diff --git a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Shell.kt b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Shell.kt index a307fc6..2cc8d30 100644 --- a/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Shell.kt +++ b/src/macosMain/kotlin/co/touchlab/xcode/cli/util/Shell.kt @@ -1,7 +1,13 @@ package co.touchlab.xcode.cli.util -import co.touchlab.kermit.Logger +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCObjectVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.coroutines.suspendCancellableCoroutine import platform.Foundation.NSData +import platform.Foundation.NSError import platform.Foundation.NSMutableData import platform.Foundation.NSPipe import platform.Foundation.NSString @@ -11,30 +17,25 @@ import platform.Foundation.NSUTF8StringEncoding import platform.Foundation.appendData import platform.Foundation.closeFile import platform.Foundation.create -import platform.Foundation.launch import platform.Foundation.launchPath import platform.Foundation.readabilityHandler +import platform.Foundation.waitUntilExit import platform.Foundation.writeData import platform.Foundation.writeabilityHandler -import platform.darwin.DISPATCH_TIME_FOREVER -import platform.darwin.dispatch_semaphore_create -import platform.darwin.dispatch_semaphore_signal -import platform.darwin.dispatch_semaphore_wait import platform.posix.EXIT_SUCCESS -import kotlin.contracts.contract +import kotlin.coroutines.resume object Shell { - private val logger = Logger.withTag("Shell") - fun exec(launchPath: String, vararg arguments: String, input: NSData? = null): ExecutionResult { + suspend fun exec(launchPath: String, vararg arguments: String, input: NSData? = null): ExecutionResult { return exec(launchPath, arguments.toList(), input) } - fun exec(launchPath: String, arguments: List, input: NSData? = null): ExecutionResult { + suspend fun exec(launchPath: String, arguments: List, input: NSData? = null): ExecutionResult { return exec(Task(launchPath, arguments, input)) } - fun exec(task: Task): ExecutionResult { - val waitHandle = dispatch_semaphore_create(0) + @OptIn(ExperimentalForeignApi::class) + suspend fun exec(task: Task): ExecutionResult { val nsTask = NSTask() nsTask.launchPath = task.launchPath nsTask.arguments = task.arguments @@ -69,18 +70,26 @@ object Shell { taskError.appendData(errorFile.availableData) } - nsTask.terminationHandler = { - inputFile?.writeabilityHandler = null - outputFile.readabilityHandler = null - errorFile.readabilityHandler = null - dispatch_semaphore_signal(waitHandle) - } + suspendCancellableCoroutine { continuation -> + nsTask.terminationHandler = { + inputFile?.writeabilityHandler = null + outputFile.readabilityHandler = null + errorFile.readabilityHandler = null + continuation.resume(Unit) + } - nsTask.launch() + memScoped { + val error = alloc>() + nsTask.launchAndReturnError(error.ptr) + } - dispatch_semaphore_wait(waitHandle, DISPATCH_TIME_FOREVER) + continuation.invokeOnCancellation { + nsTask.terminate() + nsTask.waitUntilExit() + } + } - return ExecutionResult( + val result = ExecutionResult( task = task, outputData = taskOutput, output = NSString.create(taskOutput, NSUTF8StringEncoding) as String?, @@ -88,13 +97,13 @@ object Shell { error = NSString.create(taskError, NSUTF8StringEncoding) as String?, status = nsTask.terminationStatus, reason = nsTask.terminationReason, - ).also { - if (!it.error.isNullOrBlank()) { - logger.w { - "Task $nsTask had non-empty error output:\n\n${it.error}\n" - } - } + ) + + if (!result.error.isNullOrBlank()) { + Console.warning("Task $nsTask had non-empty error output:\n\n${result.error}\n") } + + return result } class Task( @@ -137,4 +146,4 @@ object Shell { return this } } -} \ No newline at end of file +}