From b5e2f13d65c1dc4b8a17136de5ecd0554d6f13fc Mon Sep 17 00:00:00 2001 From: ya7010 <47286750+ya7010@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:34:56 +0900 Subject: [PATCH] Revert "feat(intellij): download tombi when unavailable (#2033)" This reverts commit 777456620f6fd8fde7bc400058f8be30b13327f2. --- editors/intellij/README.md | 9 +- editors/intellij/build.gradle.kts | 1 - editors/intellij/gradle/libs.versions.toml | 6 - .../tombi/configurations/Configurations.kt | 5 +- .../main/kotlin/tombi/configurations/Panel.kt | 4 +- .../tombi/server/TombiBinaryDownloader.kt | 244 ------------------ .../tombi/server/TombiBinaryResolver.kt | 77 ------ .../server/TombiServerSupportProvider.kt | 58 +---- .../main/resources/messages/tombi.properties | 1 - .../tombi/server/TombiBinaryDownloaderTest.kt | 54 ---- .../tombi/server/TombiBinaryResolverTest.kt | 115 --------- 11 files changed, 11 insertions(+), 563 deletions(-) delete mode 100644 editors/intellij/src/main/kotlin/tombi/server/TombiBinaryDownloader.kt delete mode 100644 editors/intellij/src/main/kotlin/tombi/server/TombiBinaryResolver.kt delete mode 100644 editors/intellij/src/test/kotlin/tombi/server/TombiBinaryDownloaderTest.kt delete mode 100644 editors/intellij/src/test/kotlin/tombi/server/TombiBinaryResolverTest.kt diff --git a/editors/intellij/README.md b/editors/intellij/README.md index 38caf062c..f3e42d154 100644 --- a/editors/intellij/README.md +++ b/editors/intellij/README.md @@ -6,12 +6,10 @@ ## Usage -The plugin uses a Tombi executable already available in the project or on -`PATH`. If one is not available, it downloads the latest Tombi release -automatically. +You must [have Tombi installed][2] to use this plugin. +See Tombi's documentation for more information. -You can select a specific executable in the plugin settings. Open any TOML file -to start working. +Once Tombi is installed, open any TOML file and start working. ## Logging @@ -29,6 +27,7 @@ com.intellij.platform.lsp [1]: https://tombi-toml.github.io/tombi + [2]: https://tombi-toml.github.io/tombi/docs/installation diff --git a/editors/intellij/build.gradle.kts b/editors/intellij/build.gradle.kts index a004385eb..067a169bc 100644 --- a/editors/intellij/build.gradle.kts +++ b/editors/intellij/build.gradle.kts @@ -43,7 +43,6 @@ repositories { // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { - implementation(libs.commonsCompress) testImplementation(kotlin("test")) testImplementation(libs.junit) diff --git a/editors/intellij/gradle/libs.versions.toml b/editors/intellij/gradle/libs.versions.toml index cd68ec48e..26f699121 100644 --- a/editors/intellij/gradle/libs.versions.toml +++ b/editors/intellij/gradle/libs.versions.toml @@ -1,7 +1,6 @@ [versions] # libraries annotations = "26.0.2-1" -commonsCompress = "1.28.0" junit = "4.13.2" # plugins changelog = "2.4.0" @@ -15,11 +14,6 @@ group = "org.jetbrains" name = "annotations" version.ref = "annotations" -[libraries.commonsCompress] -group = "org.apache.commons" -name = "commons-compress" -version.ref = "commonsCompress" - [libraries.junit] group = "junit" name = "junit" diff --git a/editors/intellij/src/main/kotlin/tombi/configurations/Configurations.kt b/editors/intellij/src/main/kotlin/tombi/configurations/Configurations.kt index 0c3ec36ac..ce97aa07c 100644 --- a/editors/intellij/src/main/kotlin/tombi/configurations/Configurations.kt +++ b/editors/intellij/src/main/kotlin/tombi/configurations/Configurations.kt @@ -21,8 +21,7 @@ internal class TombiConfigurations : BaseState() { /** * The Tombi executable to be used in commands. * - * When unspecified, the plugin looks for a local Tombi installation and - * downloads the latest release if none is available. + * Defaults to `tombi` if not specified. */ - var executable by string() + var executable by string("tombi") } diff --git a/editors/intellij/src/main/kotlin/tombi/configurations/Panel.kt b/editors/intellij/src/main/kotlin/tombi/configurations/Panel.kt index abfecc908..fa58ef469 100644 --- a/editors/intellij/src/main/kotlin/tombi/configurations/Panel.kt +++ b/editors/intellij/src/main/kotlin/tombi/configurations/Panel.kt @@ -47,8 +47,8 @@ private fun Row.executableInput(block: Cell.() -> Uni internal fun makePanel(state: TombiConfigurations) = panel { row(message("configurations.executable.label")) { executableInput { - bindText(state::executable.toNonNullableProperty("")) - component.emptyText.text = message("configurations.executable.placeholder") + bindText(state::executable.toNonNullableProperty("tombi")) + component.emptyText.text = "tombi" } } } diff --git a/editors/intellij/src/main/kotlin/tombi/server/TombiBinaryDownloader.kt b/editors/intellij/src/main/kotlin/tombi/server/TombiBinaryDownloader.kt deleted file mode 100644 index 498ac9e9b..000000000 --- a/editors/intellij/src/main/kotlin/tombi/server/TombiBinaryDownloader.kt +++ /dev/null @@ -1,244 +0,0 @@ -package tombi.server - -import com.intellij.openapi.application.PathManager -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream -import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream -import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream -import java.io.BufferedInputStream -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpResponse -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.AtomicMoveNotSupportedException -import java.nio.file.StandardCopyOption -import java.nio.file.attribute.PosixFilePermission -import java.time.Duration - - -internal object TombiBinaryDownloader { - private const val RELEASES_URL = "https://github.com/tombi-toml/tombi/releases" - private val versionPattern = Regex("""v?(\d+\.\d+\.\d+)""") - - @Synchronized - fun downloadLatestOrCached( - cacheDirectory: Path = PathManager.getSystemDir().resolve("tombi"), - client: HttpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(15)) - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(), - osName: String = System.getProperty("os.name"), - architecture: String = System.getProperty("os.arch"), - ): String { - return try { - downloadLatest(cacheDirectory, client, platform(osName, architecture)).toString() - } catch (error: Exception) { - newestCachedBinary(cacheDirectory)?.toString() ?: throw error - } - } - - internal fun platform(osName: String, architecture: String): Platform { - val targetArchitecture = when (architecture.lowercase()) { - "aarch64", "arm64" -> "aarch64" - "x86_64", "amd64", "x64" -> "x86_64" - else -> error("Unsupported architecture: $architecture") - } - val normalizedOsName = osName.lowercase() - val (targetOs, archiveType) = when { - normalizedOsName.startsWith("mac") -> - "apple-darwin" to ArchiveType.TAR_GZ - normalizedOsName.startsWith("linux") -> - "unknown-linux-musl" to ArchiveType.TAR_GZ - normalizedOsName.startsWith("windows") -> - "pc-windows-msvc" to ArchiveType.ZIP - else -> error("Unsupported operating system: $osName") - } - - return Platform( - target = "$targetArchitecture-$targetOs", - binaryName = if (archiveType == ArchiveType.ZIP) "tombi.exe" else "tombi", - archiveType = archiveType, - ) - } - - internal fun versionFromLatestReleaseUri(uri: URI): String { - val tag = uri.path.substringAfterLast('/') - return versionPattern.matchEntire(tag)?.groupValues?.get(1) - ?: error("Unexpected latest release URL: $uri") - } - - private fun downloadLatest( - cacheDirectory: Path, - client: HttpClient, - platform: Platform, - ): Path { - val latestRequest = HttpRequest.newBuilder(URI.create("$RELEASES_URL/latest")) - .timeout(Duration.ofSeconds(30)) - .GET() - .build() - val latestResponse = client.send(latestRequest, HttpResponse.BodyHandlers.discarding()) - requireSuccess(latestResponse.statusCode(), latestResponse.uri()) - - val version = versionFromLatestReleaseUri(latestResponse.uri()) - val versionDirectory = cacheDirectory.resolve("tombi-$version") - val binaryPath = versionDirectory.resolve(platform.binaryName) - if (Files.isRegularFile(binaryPath)) { - return binaryPath - } - - Files.createDirectories(versionDirectory) - val assetName = "tombi-cli-$version-${platform.target}.${platform.archiveType.extension}" - val assetUri = URI.create("$RELEASES_URL/download/v$version/$assetName") - val archivePath = Files.createTempFile(versionDirectory, "download-", ".${platform.archiveType.extension}") - val extractedPath = Files.createTempFile(versionDirectory, "tombi-", ".tmp") - - try { - val assetRequest = HttpRequest.newBuilder(assetUri) - .timeout(Duration.ofMinutes(2)) - .GET() - .build() - val assetResponse = client.send(assetRequest, HttpResponse.BodyHandlers.ofFile(archivePath)) - requireSuccess(assetResponse.statusCode(), assetResponse.uri()) - - BufferedInputStream(Files.newInputStream(archivePath)).use { input -> - Files.newOutputStream(extractedPath).use { output -> - when (platform.archiveType) { - ArchiveType.TAR_GZ -> - extractFromTarGz(input, platform.binaryName, output) - ArchiveType.ZIP -> - extractFromZip(input, platform.binaryName, output) - } - } - } - makeExecutable(extractedPath) - moveIntoPlace(extractedPath, binaryPath) - } finally { - Files.deleteIfExists(archivePath) - Files.deleteIfExists(extractedPath) - } - - return binaryPath - } - - private fun extractFromTarGz( - input: java.io.InputStream, - binaryName: String, - output: java.io.OutputStream, - ) { - TarArchiveInputStream(GzipCompressorInputStream(input)).use { archive -> - while (true) { - val entry = archive.nextEntry ?: break - if (entry.isFile && Path.of(entry.name).fileName.toString() == binaryName) { - archive.copyTo(output) - return - } - } - } - error("$binaryName was not found in the downloaded archive") - } - - private fun extractFromZip( - input: java.io.InputStream, - binaryName: String, - output: java.io.OutputStream, - ) { - ZipArchiveInputStream(input).use { archive -> - while (true) { - val entry = archive.nextEntry ?: break - if (!entry.isDirectory && Path.of(entry.name).fileName.toString() == binaryName) { - archive.copyTo(output) - return - } - } - } - error("$binaryName was not found in the downloaded archive") - } - - private fun makeExecutable(binaryPath: Path) { - try { - val permissions = Files.getPosixFilePermissions(binaryPath) - permissions.addAll( - setOf( - PosixFilePermission.OWNER_EXECUTE, - PosixFilePermission.GROUP_EXECUTE, - PosixFilePermission.OTHERS_EXECUTE, - ), - ) - Files.setPosixFilePermissions(binaryPath, permissions) - } catch (_: UnsupportedOperationException) { - binaryPath.toFile().setExecutable(true, false) - } - } - - private fun moveIntoPlace(source: Path, target: Path) { - try { - Files.move( - source, - target, - StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING, - ) - } catch (_: AtomicMoveNotSupportedException) { - Files.move(source, target, StandardCopyOption.REPLACE_EXISTING) - } - } - - private fun newestCachedBinary(cacheDirectory: Path): Path? { - if (!Files.isDirectory(cacheDirectory)) { - return null - } - - return Files.list(cacheDirectory).use { entries -> - entries - .map { directory -> - val version = versionPattern.matchEntire(directory.fileName.toString().removePrefix("tombi-")) - ?.groupValues - ?.get(1) - val binary = listOf("tombi", "tombi.exe") - .map(directory::resolve) - .firstOrNull(Files::isRegularFile) - if (version != null && binary != null) { - Triple(versionComponents(version), version, binary) - } else { - null - } - } - .filter { it != null } - .map { it!! } - .max { left, right -> compareVersions(left.first, right.first) } - .orElse(null) - ?.third - } - } - - private fun versionComponents(version: String): List = - version.split('.').map(String::toInt) - - private fun compareVersions(left: List, right: List): Int { - for (index in 0 until maxOf(left.size, right.size)) { - val comparison = left.getOrElse(index) { 0 }.compareTo(right.getOrElse(index) { 0 }) - if (comparison != 0) { - return comparison - } - } - return 0 - } - - private fun requireSuccess(statusCode: Int, uri: URI) { - check(statusCode in 200..299) { - "Request to $uri failed with HTTP $statusCode" - } - } - - internal data class Platform( - val target: String, - val binaryName: String, - val archiveType: ArchiveType, - ) - - internal enum class ArchiveType(val extension: String) { - TAR_GZ("tar.gz"), - ZIP("zip"), - } -} diff --git a/editors/intellij/src/main/kotlin/tombi/server/TombiBinaryResolver.kt b/editors/intellij/src/main/kotlin/tombi/server/TombiBinaryResolver.kt deleted file mode 100644 index f83a596a1..000000000 --- a/editors/intellij/src/main/kotlin/tombi/server/TombiBinaryResolver.kt +++ /dev/null @@ -1,77 +0,0 @@ -package tombi.server - -import com.intellij.util.EnvironmentUtil -import java.nio.file.Files -import java.nio.file.Path - - -internal object TombiBinaryResolver { - fun resolveLocal( - projectPath: Path?, - configuredExecutable: String?, - environment: Map = EnvironmentUtil.getEnvironmentMap(), - osName: String = System.getProperty("os.name"), - userHome: String = System.getProperty("user.home"), - ): String? { - configuredExecutable - ?.trim() - ?.takeIf(String::isNotEmpty) - ?.let { return expandHome(it, userHome) } - - val isWindows = osName.startsWith("Windows", ignoreCase = true) - val binaryName = if (isWindows) "tombi.exe" else "tombi" - - if (projectPath != null) { - val virtualEnvironmentDirectory = if (isWindows) "Scripts" else "bin" - val projectCandidates = mutableListOf( - projectPath.resolve(".venv").resolve(virtualEnvironmentDirectory).resolve(binaryName), - projectPath.resolve("node_modules").resolve(".bin").resolve(binaryName), - ) - if (isWindows) { - projectCandidates.addAll( - listOf("tombi.cmd", "tombi.ps1").map { - projectPath.resolve("node_modules").resolve(".bin").resolve(it) - }, - ) - } - projectCandidates.firstOrNull(Files::isRegularFile)?.let { - return it.toString() - } - } - - return findOnPath(binaryName, environment, isWindows) - } - - private fun expandHome(path: String, userHome: String): String = - when { - path == "~" -> userHome - path.startsWith("~/") || path.startsWith("~\\") -> userHome + path.drop(1) - else -> path - } - - private fun findOnPath( - binaryName: String, - environment: Map, - isWindows: Boolean, - ): String? { - val pathValue = environment.entries - .firstOrNull { (key, _) -> key.equals("PATH", ignoreCase = isWindows) } - ?.value - ?: return null - - val separator = if (isWindows) ';' else ':' - val candidateNames = if (isWindows) { - listOf(binaryName, "tombi.cmd", "tombi.bat") - } else { - listOf(binaryName) - } - - return pathValue - .split(separator) - .asSequence() - .filter(String::isNotBlank) - .flatMap { directory -> candidateNames.asSequence().map(Path.of(directory)::resolve) } - .firstOrNull(Files::isRegularFile) - ?.toString() - } -} diff --git a/editors/intellij/src/main/kotlin/tombi/server/TombiServerSupportProvider.kt b/editors/intellij/src/main/kotlin/tombi/server/TombiServerSupportProvider.kt index 8d8412635..f87c7827e 100644 --- a/editors/intellij/src/main/kotlin/tombi/server/TombiServerSupportProvider.kt +++ b/editors/intellij/src/main/kotlin/tombi/server/TombiServerSupportProvider.kt @@ -1,22 +1,14 @@ package tombi.server -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.platform.lsp.api.LspServer -import com.intellij.platform.lsp.api.LspServerManager import com.intellij.platform.lsp.api.LspServerSupportProvider import com.intellij.platform.lsp.api.LspServerSupportProvider.LspServerStarter import com.intellij.platform.lsp.api.lsWidget.LspServerWidgetItem -import com.intellij.util.concurrency.AppExecutorUtil import tombi.Icons import tombi.configurations.TombiConfigurable import tombi.configurations.tombiConfigurations -import java.nio.file.Files -import java.nio.file.Path -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap /** @@ -33,54 +25,10 @@ internal class TombiServerSupportProvider : LspServerSupportProvider { LspServerWidgetItem(lspServer, currentFile, Icons._16, TombiConfigurable::class.java) override fun fileOpened(project: Project, file: VirtualFile, serverStarter: LspServerStarter) { - if (!file.isTOMLFile) { - return - } - - val configuredExecutable = tombiConfigurations.executable - TombiBinaryResolver.resolveLocal(project.path, configuredExecutable)?.let { executable -> + if (file.isTOMLFile) { + val executable = tombiConfigurations.executable ?: return serverStarter.ensureServerStarted(TombiServerDescriptor(project, executable)) - return - } - - managedExecutable - ?.takeIf { Files.isRegularFile(Path.of(it)) } - ?.let { executable -> - serverStarter.ensureServerStarted(TombiServerDescriptor(project, executable)) - return - } - - val resolution = managedResolutions.computeIfAbsent(project) { - CompletableFuture.supplyAsync( - { TombiBinaryDownloader.downloadLatestOrCached() }, - AppExecutorUtil.getAppExecutorService(), - ) - } - - resolution.whenComplete { executable, error -> - managedResolutions.remove(project, resolution) - - if (error != null) { - LOG.warn("Failed to obtain a Tombi language server", error) - return@whenComplete - } - - managedExecutable = executable - ApplicationManager.getApplication().invokeLater { - if (!project.isDisposed) { - LspServerManager.getInstance(project).ensureServerStarted( - TombiServerSupportProvider::class.java, - TombiServerDescriptor(project, executable), - ) - } - } } } - - companion object { - private val LOG = Logger.getInstance(TombiServerSupportProvider::class.java) - private val managedResolutions = ConcurrentHashMap>() - @Volatile - private var managedExecutable: String? = null - } + } diff --git a/editors/intellij/src/main/resources/messages/tombi.properties b/editors/intellij/src/main/resources/messages/tombi.properties index 2e087c705..4315b0809 100644 --- a/editors/intellij/src/main/resources/messages/tombi.properties +++ b/editors/intellij/src/main/resources/messages/tombi.properties @@ -2,4 +2,3 @@ languageServer.presentableName = Tombi configurations.displayName = Tombi configurations.executable.label = Executable: -configurations.executable.placeholder = Automatic diff --git a/editors/intellij/src/test/kotlin/tombi/server/TombiBinaryDownloaderTest.kt b/editors/intellij/src/test/kotlin/tombi/server/TombiBinaryDownloaderTest.kt deleted file mode 100644 index 007cdd150..000000000 --- a/editors/intellij/src/test/kotlin/tombi/server/TombiBinaryDownloaderTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package tombi.server - -import java.net.URI -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - - -class TombiBinaryDownloaderTest { - @Test - fun `resolves supported release targets`() { - assertEquals( - TombiBinaryDownloader.Platform( - target = "aarch64-apple-darwin", - binaryName = "tombi", - archiveType = TombiBinaryDownloader.ArchiveType.TAR_GZ, - ), - TombiBinaryDownloader.platform("Mac OS X", "arm64"), - ) - assertEquals( - TombiBinaryDownloader.Platform( - target = "x86_64-unknown-linux-musl", - binaryName = "tombi", - archiveType = TombiBinaryDownloader.ArchiveType.TAR_GZ, - ), - TombiBinaryDownloader.platform("Linux", "amd64"), - ) - assertEquals( - TombiBinaryDownloader.Platform( - target = "aarch64-pc-windows-msvc", - binaryName = "tombi.exe", - archiveType = TombiBinaryDownloader.ArchiveType.ZIP, - ), - TombiBinaryDownloader.platform("Windows 11", "aarch64"), - ) - } - - @Test - fun `extracts version from latest release redirect`() { - assertEquals( - "1.2.4", - TombiBinaryDownloader.versionFromLatestReleaseUri( - URI.create("https://github.com/tombi-toml/tombi/releases/tag/v1.2.4"), - ), - ) - } - - @Test - fun `rejects unsupported platform`() { - assertFailsWith { - TombiBinaryDownloader.platform("FreeBSD", "x86_64") - } - } -} diff --git a/editors/intellij/src/test/kotlin/tombi/server/TombiBinaryResolverTest.kt b/editors/intellij/src/test/kotlin/tombi/server/TombiBinaryResolverTest.kt deleted file mode 100644 index bc0df125a..000000000 --- a/editors/intellij/src/test/kotlin/tombi/server/TombiBinaryResolverTest.kt +++ /dev/null @@ -1,115 +0,0 @@ -package tombi.server - -import java.nio.file.Files -import kotlin.io.path.createDirectories -import kotlin.io.path.createFile -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - - -class TombiBinaryResolverTest { - @Test - fun `configured executable takes priority and expands home`() { - assertEquals( - "/home/test/bin/tombi", - TombiBinaryResolver.resolveLocal( - projectPath = null, - configuredExecutable = "~/bin/tombi", - environment = emptyMap(), - osName = "Linux", - userHome = "/home/test", - ), - ) - } - - @Test - fun `project virtual environment takes priority over node modules and path`() { - val project = Files.createTempDirectory("tombi-project") - val venvTombi = project.resolve(".venv/bin/tombi") - val nodeModulesTombi = project.resolve("node_modules/.bin/tombi") - val pathDirectory = Files.createTempDirectory("tombi-path") - val pathTombi = pathDirectory.resolve("tombi") - venvTombi.parent.createDirectories() - nodeModulesTombi.parent.createDirectories() - venvTombi.createFile() - nodeModulesTombi.createFile() - pathTombi.createFile() - - assertEquals( - venvTombi.toString(), - TombiBinaryResolver.resolveLocal( - projectPath = project, - configuredExecutable = null, - environment = mapOf("PATH" to pathDirectory.toString()), - osName = "Linux", - ), - ) - } - - @Test - fun `node modules takes priority over path`() { - val project = Files.createTempDirectory("tombi-project") - val nodeModulesTombi = project.resolve("node_modules/.bin/tombi") - val pathDirectory = Files.createTempDirectory("tombi-path") - nodeModulesTombi.parent.createDirectories() - nodeModulesTombi.createFile() - pathDirectory.resolve("tombi").createFile() - - assertEquals( - nodeModulesTombi.toString(), - TombiBinaryResolver.resolveLocal( - projectPath = project, - configuredExecutable = null, - environment = mapOf("PATH" to pathDirectory.toString()), - osName = "Linux", - ), - ) - } - - @Test - fun `returns path installation when no project installation exists`() { - val pathDirectory = Files.createTempDirectory("tombi-path") - val pathTombi = pathDirectory.resolve("tombi").createFile() - - assertEquals( - pathTombi.toString(), - TombiBinaryResolver.resolveLocal( - projectPath = Files.createTempDirectory("tombi-project"), - configuredExecutable = null, - environment = mapOf("PATH" to pathDirectory.toString()), - osName = "Linux", - ), - ) - } - - @Test - fun `finds node modules command shim on Windows`() { - val project = Files.createTempDirectory("tombi-project") - val nodeModulesTombi = project.resolve("node_modules/.bin/tombi.cmd") - nodeModulesTombi.parent.createDirectories() - nodeModulesTombi.createFile() - - assertEquals( - nodeModulesTombi.toString(), - TombiBinaryResolver.resolveLocal( - projectPath = project, - configuredExecutable = null, - environment = emptyMap(), - osName = "Windows 11", - ), - ) - } - - @Test - fun `returns null when no local installation exists`() { - assertNull( - TombiBinaryResolver.resolveLocal( - projectPath = Files.createTempDirectory("tombi-project"), - configuredExecutable = null, - environment = emptyMap(), - osName = "Linux", - ), - ) - } -}