diff --git a/src/main/kotlin/com/squareup/cash/hermit/Hermit.kt b/src/main/kotlin/com/squareup/cash/hermit/Hermit.kt index 64ea4a2..137a390 100644 --- a/src/main/kotlin/com/squareup/cash/hermit/Hermit.kt +++ b/src/main/kotlin/com/squareup/cash/hermit/Hermit.kt @@ -205,6 +205,10 @@ object Hermit { return bin?.findChild(exe)?.exists() ?: false } + fun findPackage(type: PackageType): HermitPackage? { + return this.properties?.packages?.find { it.type == type } + } + private fun clear() { this.properties = null this.isHermitProject = false @@ -236,4 +240,4 @@ object Hermit { return ImmutableMap.copyOf(env) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/squareup/cash/hermit/gradle/HermitBuildLayoutParameters.kt b/src/main/kotlin/com/squareup/cash/hermit/gradle/HermitBuildLayoutParameters.kt new file mode 100644 index 0000000..b1fc588 --- /dev/null +++ b/src/main/kotlin/com/squareup/cash/hermit/gradle/HermitBuildLayoutParameters.kt @@ -0,0 +1,72 @@ +package com.squareup.cash.hermit.gradle + +import com.intellij.execution.target.value.TargetValue +import com.intellij.openapi.project.Project +import org.gradle.util.GradleVersion +import org.jetbrains.plugins.gradle.service.GradleInstallationManager +import org.jetbrains.plugins.gradle.service.execution.BuildLayoutParameters +import org.jetbrains.plugins.gradle.service.execution.gradleUserHomeDir +import org.jetbrains.plugins.gradle.settings.GradleSettings +import java.nio.file.Files +import java.nio.file.Path +import java.util.regex.Pattern + +private val GRADLE_JAR_PATTERN = Pattern.compile(System.getProperty("gradle.pattern.core.jar", "gradle-(core-)?(\\d.*)\\.jar")) + +/** + * [BuildLayoutParameters] that points to a hermit-managed Gradle installation. + * The [gradleUserHomePath] resolution matches IntelliJ's [LocalBuildLayoutParameters]: + * project Gradle settings first, then GRADLE_USER_HOME env/property, then ~/.gradle. + */ +class HermitBuildLayoutParameters( + private val hermitGradleHome: Path, + project: Project +) : BuildLayoutParameters { + + override val gradleHome: TargetValue = TargetValue.fixed(hermitGradleHome) + + override val gradleVersion: GradleVersion? by lazy { + detectGradleVersion(hermitGradleHome) + } + + override val gradleUserHomePath: TargetValue by lazy { + val serviceDir = GradleSettings.getInstance(project).serviceDirectoryPath + val userHome = if (serviceDir != null) { + Path.of(serviceDir) + } else { + gradleUserHomeDir().toPath() + } + TargetValue.fixed(userHome) + } +} + +/** + * Detects the Gradle version from a Gradle home directory by inspecting jar filenames in `lib/`. + * Inlined from [GradleInstallationManager] to avoid binary compatibility issues with the Companion + * object across older IntelliJ versions. + */ +private fun detectGradleVersion(gradleHome: Path): GradleVersion? { + val libs = gradleHome.resolve("lib") + if (!Files.isDirectory(libs)) return null + + val versionString = try { + Files.list(libs).use { children -> + children + .map { GRADLE_JAR_PATTERN.matcher(it.fileName.toString()) } + .filter { it.matches() } + .map { it.group(2) } + .findFirst() + .orElse(null) + } + } catch (_: Exception) { + null + } ?: return null + + return try { + GradleVersion.version(versionString) + } catch (_: IllegalArgumentException) { + // GradleVersion.version(gradleVersion) might throw exception for custom Gradle versions + // https://youtrack.jetbrains.com/issue/IDEA-216892 + null + } +} diff --git a/src/main/kotlin/com/squareup/cash/hermit/gradle/HermitGradleExecutionAware.kt b/src/main/kotlin/com/squareup/cash/hermit/gradle/HermitGradleExecutionAware.kt index befbdce..d90e2b8 100644 --- a/src/main/kotlin/com/squareup/cash/hermit/gradle/HermitGradleExecutionAware.kt +++ b/src/main/kotlin/com/squareup/cash/hermit/gradle/HermitGradleExecutionAware.kt @@ -1,3 +1,5 @@ +@file:Suppress("UnstableApiUsage") + package com.squareup.cash.hermit.gradle import com.intellij.openapi.diagnostic.Logger @@ -5,9 +7,10 @@ import com.intellij.openapi.externalSystem.model.task.ExternalSystemTask import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener import com.intellij.openapi.project.Project import com.squareup.cash.hermit.Hermit -import org.jetbrains.annotations.ApiStatus +import com.squareup.cash.hermit.PackageType import org.jetbrains.plugins.gradle.service.execution.BuildLayoutParameters import org.jetbrains.plugins.gradle.service.execution.GradleExecutionAware +import java.nio.file.Path class HermitGradleExecutionAware: GradleExecutionAware { private val log: Logger = Logger.getInstance(this.javaClass) @@ -42,13 +45,32 @@ class HermitGradleExecutionAware: GradleExecutionAware { } } - override fun getBuildLayoutParameters(project: Project, projectPath: String): BuildLayoutParameters? = null + override fun getBuildLayoutParameters(project: Project, projectPath: Path): BuildLayoutParameters? { + val gradleHome = hermitGradleHome(project) ?: return null + log.debug("using hermit Gradle home: $gradleHome") + return HermitBuildLayoutParameters(gradleHome, project) + } + + override fun getDefaultBuildLayoutParameters(project: Project): BuildLayoutParameters? { + val gradleHome = hermitGradleHome(project) ?: return null + log.debug("using hermit Gradle home for default build layout: $gradleHome") + return HermitBuildLayoutParameters(gradleHome, project) + } - override fun getDefaultBuildLayoutParameters(project: Project): BuildLayoutParameters? = null + override fun isGradleInstallationHomeDir(project: Project, homePath: Path): Boolean { + val gradleHome = hermitGradleHome(project) ?: return false + return homePath == gradleHome + } - override fun isGradleInstallationHomeDir(project: Project, homePath: String): Boolean = false + private fun hermitGradleHome(project: Project): Path? { + val state = Hermit(project) + if (!state.hasHermit() || state.hermitStatus() == Hermit.HermitStatus.Disabled) return null + val gradlePkg = state.findPackage(PackageType.Gradle) ?: return null + val path = Path.of(gradlePkg.path) + return if (path.toFile().isDirectory) path else null + } companion object { const val TIMEOUT_MS = 120000 } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/squareup/cash/hermit/idea/HermitJdkUpdater.kt b/src/main/kotlin/com/squareup/cash/hermit/idea/HermitJdkUpdater.kt index 98b597e..bebdd13 100644 --- a/src/main/kotlin/com/squareup/cash/hermit/idea/HermitJdkUpdater.kt +++ b/src/main/kotlin/com/squareup/cash/hermit/idea/HermitJdkUpdater.kt @@ -4,15 +4,20 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.ProjectJdkTable -import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl -import com.intellij.openapi.util.Disposer import com.squareup.cash.hermit.* +import java.io.File class HermitJdkUpdater : HermitPropertyHandler { private val log: Logger = Logger.getInstance(this.javaClass) override fun handle(hermitPackage: HermitPackage, project: Project) { if (hermitPackage.type == PackageType.JDK) { + if (!File(hermitPackage.path).isDirectory) { + log.warn("JDK path does not exist: ${hermitPackage.path}, skipping SDK configuration") + UI.showError(project, "Hermit JDK path does not exist: ${hermitPackage.path}. " + + "Try running hermit install from the terminal.") + return + } val projectSdk = project.projectSdk() if (hermitPackage.sdkName() != projectSdk?.name) { log.debug("setting project (" + project.name + ") SDK to " + hermitPackage.logString()) diff --git a/src/test/kotlin/com/squareup/cash/hermit/FakeHermit.kt b/src/test/kotlin/com/squareup/cash/hermit/FakeHermit.kt index 0f5d93c..bb38b52 100644 --- a/src/test/kotlin/com/squareup/cash/hermit/FakeHermit.kt +++ b/src/test/kotlin/com/squareup/cash/hermit/FakeHermit.kt @@ -35,14 +35,11 @@ object BrokenHermit : AbstractHermit() { data class FakeHermit(val packages: List) : AbstractHermit() { override fun writeTo(path: Path) { - val packageList = packages - .map { "echo \"${it.name}\"" } - .joinToString("\n") - val envList = packages - .flatMap { it.env.entries } - .map { entry -> "echo \"${entry.key}=${entry.value}\""} - .joinToString("\n") - val infoList = JsonArray(packages + val packageList = packages.joinToString("\n") { "echo \"${it.name}\"" } + val envList = packages + .flatMap { it.env.entries } + .joinToString("\n") { entry -> "echo \"${entry.key}=${entry.value}\"" } + val infoList = JsonArray(packages .map { p -> JsonObject(mapOf( "Reference" to JsonObject(mapOf( "Name" to JsonPrimitive(p.name), @@ -54,13 +51,13 @@ data class FakeHermit(val packages: List) : AbstractHermit() { )) }).toString() val listBlock = if (packageList.isNotEmpty()) """ - if [ ${'$'}1 == "list" ]; then + if [ $1 == "list" ]; then $packageList fi """.trimIndent() else "" val envBlock = if (envList.isNotEmpty()) """ - if [ ${'$'}1 == "env" ]; then + if [ $1 == "env" ]; then $envList fi """.trimIndent() @@ -85,4 +82,4 @@ fun String.runCommand(workingDir: Path) { builder.environment()["HERMIT_ENV"] = workingDir.toString() builder.start().waitFor(60, TimeUnit.SECONDS) -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/squareup/cash/hermit/gradle/HermitGradleExecutionAwareTest.kt b/src/test/kotlin/com/squareup/cash/hermit/gradle/HermitGradleExecutionAwareTest.kt new file mode 100644 index 0000000..5202a9a --- /dev/null +++ b/src/test/kotlin/com/squareup/cash/hermit/gradle/HermitGradleExecutionAwareTest.kt @@ -0,0 +1,138 @@ +package com.squareup.cash.hermit.gradle + +import com.intellij.execution.target.value.TargetValue +import com.squareup.cash.hermit.FakeHermit +import com.squareup.cash.hermit.Hermit +import com.squareup.cash.hermit.HermitProjectTestCase +import com.squareup.cash.hermit.PackageType +import com.squareup.cash.hermit.TestPackage +import junit.framework.TestCase +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path + +class HermitGradleExecutionAwareTest : HermitProjectTestCase() { + private val aware = HermitGradleExecutionAware() + + private fun gradleRoot(): Path { + val dir = projectDirOrFile.parent.resolve("fake-gradle-home") + Files.createDirectories(dir) + return dir + } + + private fun localValue(targetValue: TargetValue): T? { + return targetValue.localValue.blockingGet(0) + } + + @Test fun `test getBuildLayoutParameters returns null when hermit has no gradle package`() { + withHermit(FakeHermit(listOf(TestPackage("openjdk", "21", "", "/nonexistent/jdk/path", emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + val params = aware.getBuildLayoutParameters(project, projectDirOrFile.parent) + assertNull(params) + } + + @Test fun `test getBuildLayoutParameters returns null when hermit is not enabled`() { + val params = aware.getBuildLayoutParameters(project, projectDirOrFile.parent) + assertNull(params) + } + + @Test + fun `test getBuildLayoutParameters returns hermit gradle home when gradle package exists`() { + val root = gradleRoot() + withHermit(FakeHermit(listOf(TestPackage("gradle", "9.3.1", "", root.toString(), emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + val params = aware.getBuildLayoutParameters(project, projectDirOrFile.parent)!! + assertEquals(root, localValue(params.gradleHome!!)) + } + + @Test fun `test getDefaultBuildLayoutParameters returns null when no gradle package`() { + withHermit(FakeHermit(emptyList())) + Hermit(project).enable() + waitAppThreads() + + val params = aware.getDefaultBuildLayoutParameters(project) + assertNull(params) + } + + @Test + fun `test getDefaultBuildLayoutParameters returns hermit gradle home when gradle package exists`() { + val root = gradleRoot() + withHermit(FakeHermit(listOf(TestPackage("gradle", "9.3.1", "", root.toString(), emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + val params = aware.getDefaultBuildLayoutParameters(project)!! + assertEquals(root, localValue(params.gradleHome!!)) + } + + @Test fun `test isGradleInstallationHomeDir returns true for hermit gradle home`() { + val root = gradleRoot() + withHermit(FakeHermit(listOf(TestPackage("gradle", "9.3.1", "", root.toString(), emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + assertTrue(aware.isGradleInstallationHomeDir(project, root)) + } + + @Test fun `test isGradleInstallationHomeDir returns false for non-hermit path`() { + val root = gradleRoot() + withHermit(FakeHermit(listOf(TestPackage("gradle", "9.3.1", "", root.toString(), emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + assertFalse(aware.isGradleInstallationHomeDir(project, Path.of("/some/other/path"))) + } + + @Test fun `test isGradleInstallationHomeDir returns false when hermit has no gradle`() { + withHermit(FakeHermit(emptyList())) + Hermit(project).enable() + waitAppThreads() + + assertFalse(aware.isGradleInstallationHomeDir(project, Path.of("/some/path"))) + } + + @Test fun `test gradlePackage returns the gradle package after enable`() { + val root = gradleRoot() + withHermit(FakeHermit(listOf(TestPackage("gradle", "9.3.1", "", root.toString(), emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + val pkg = Hermit(project).findPackage(PackageType.Gradle)!! + TestCase.assertEquals("gradle", pkg.name) + TestCase.assertEquals("9.3.1", pkg.version) + TestCase.assertEquals(root.toString(), pkg.path) + } + + @Test fun `test gradlePackage returns null when no gradle package`() { + withHermit(FakeHermit(listOf(TestPackage("openjdk", "21", "", "/nonexistent/jdk/path", emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + assertNull(Hermit(project).findPackage(PackageType.Gradle)) + } + + @Test fun `test build layout parameters provides gradle user home`() { + val root = gradleRoot() + withHermit(FakeHermit(listOf(TestPackage("gradle", "9.3.1", "", root.toString(), emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + val params = aware.getBuildLayoutParameters(project, projectDirOrFile.parent)!! + val userHome = localValue(params.gradleUserHomePath) + assertNotNull(userHome) + } + + @Test fun `test getBuildLayoutParameters returns null when gradle path does not exist`() { + val nonExistent = "/tmp/hermit-test-nonexistent-gradle-home-${System.nanoTime()}" + withHermit(FakeHermit(listOf(TestPackage("gradle", "9.3.1", "", nonExistent, emptyMap())))) + Hermit(project).enable() + waitAppThreads() + + val params = aware.getBuildLayoutParameters(project, projectDirOrFile.parent) + assertNull(params) + } +}