From 0319db1d06dd65f09a12e1a91b5de758fa2d2883 Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov <654232+AlexeyTsvetkov@users.noreply.github.com> Date: Fri, 26 May 2023 22:02:21 +0300 Subject: [PATCH] Improve error messages when checking tasks (#3194) * Improve error messages when checking tasks Previously some errors in checkRuntime task were reported as a nested exception. By default, Gradle shows only top-level error message of an exception, which made some errors confusing. For example, when javac was missing from JDK, Gradle only showed "Could not infer Java runtime version for Java home directory". The part that said javac was missing was only shown, when Gradle was run with --stacktrace argument. This is suboptimal UX, so this commit refactors checkRuntime to make error messages more descriptive. #3133 * Handle JDK 1.8 correctly * Prebuild jdk version probe --- gradle-plugins/build.gradle.kts | 5 +- .../internal/configureJvmApplication.kt | 12 +- .../AbstractCheckNativeDistributionRuntime.kt | 128 ++++++------------ .../AbstractConfigureDesktopPreviewTask.kt | 29 ++-- .../configureExperimentalWebApplication.kt | 8 +- .../compose/internal/utils/gradleUtils.kt | 26 ++++ .../jdk-version-probe/build.gradle.kts | 14 ++ .../application/internal/JdkVersionProbe.java | 38 ++++++ gradle-plugins/settings.gradle.kts | 1 + 9 files changed, 157 insertions(+), 104 deletions(-) create mode 100644 gradle-plugins/jdk-version-probe/build.gradle.kts create mode 100644 gradle-plugins/jdk-version-probe/src/main/java/org/jetbrains/compose/desktop/application/internal/JdkVersionProbe.java diff --git a/gradle-plugins/build.gradle.kts b/gradle-plugins/build.gradle.kts index 67e1db1fa5..bdc8d51c87 100644 --- a/gradle-plugins/build.gradle.kts +++ b/gradle-plugins/build.gradle.kts @@ -128,7 +128,10 @@ fun Project.configureGradlePlugin( } tasks.register("publishToMavenLocal") { + val publishToMavenLocal = this for (subproject in subprojects) { - dependsOn(subproject.tasks.named("publishToMavenLocal")) + subproject.plugins.withId("maven-publish") { + publishToMavenLocal.dependsOn("${subproject.path}:publishToMavenLocal") + } } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index e248b01fcd..5f9a541bd1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -17,6 +17,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.internal.validation.validatePackageVersions import org.jetbrains.compose.desktop.application.tasks.* import org.jetbrains.compose.desktop.tasks.AbstractUnpackDefaultComposeApplicationResourcesTask +import org.jetbrains.compose.internal.utils.* import org.jetbrains.compose.internal.utils.OS import org.jetbrains.compose.internal.utils.currentOS import org.jetbrains.compose.internal.utils.currentTarget @@ -65,7 +66,12 @@ private fun JvmApplicationContext.configureCommonJvmDesktopTasks(): CommonJvmDes taskNameAction = "check", taskNameObject = "runtime" ) { - javaHome.set(app.javaHomeProvider) + jdkHome.set(app.javaHomeProvider) + jdkVersionProbeJar.from( + project.detachedComposeGradleDependency( + artifactId = "gradle-plugin-internal-jdk-version-probe" + ).excludeTransitiveDependencies() + ) } val suggestRuntimeModules = tasks.register( @@ -249,9 +255,7 @@ private fun JvmApplicationContext.configureProguardTask( mainClass.set(app.mainClass) proguardVersion.set(settings.version) proguardFiles.from(proguardVersion.map { proguardVersion -> - project.configurations.detachedConfiguration( - project.dependencies.create("com.guardsquare:proguard-gradle:${proguardVersion}") - ) + project.detachedDependency(groupId = "com.guardsquare", artifactId = "proguard-gradle", version = proguardVersion) }) configurationFiles.from(settings.configurationFiles) // ProGuard uses -dontobfuscate option to turn off obfuscation, which is enabled by default diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt index 3e33619b59..68dd0518f4 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNativeDistributionRuntime.kt @@ -5,7 +5,7 @@ package org.jetbrains.compose.desktop.application.tasks -import org.gradle.api.file.Directory +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.RegularFile import org.gradle.api.provider.Property import org.gradle.api.provider.Provider @@ -16,7 +16,6 @@ import org.jetbrains.compose.internal.utils.ioFile import org.jetbrains.compose.internal.utils.notNullProperty import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask -import org.jetbrains.compose.internal.utils.clearDirs import java.io.File // __COMPOSE_NATIVE_DISTRIBUTIONS_MIN_JAVA_VERSION__ @@ -24,53 +23,67 @@ internal const val MIN_JAVA_RUNTIME_VERSION = 17 @CacheableTask abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTask() { + @get:Classpath + val jdkVersionProbeJar: ConfigurableFileCollection = objects.fileCollection() + @get:PathSensitive(PathSensitivity.ABSOLUTE) @get:InputDirectory - val javaHome: Property = objects.notNullProperty() + val jdkHome: Property = objects.notNullProperty() private val taskDir = project.layout.buildDirectory.dir("compose/tmp/$name") @get:OutputFile val javaRuntimePropertiesFile: Provider = taskDir.map { it.file("properties.bin") } - @get:LocalState - val workingDir: Provider = taskDir.map { it.dir("localState") } + private val jdkHomeFile: File + get() = File(jdkHome.orNull ?: error("Missing jdkHome value")) + + private fun File.getJdkTool(toolName: String): File = + resolve("bin/${executableName(toolName)}") + + private fun ensureToolsExist(vararg tools: File) { + val missingTools = tools.filter { !it.exists() }.map { "'${it.name}'" } - private val javaExec: File - get() = getTool("java") + if (missingTools.isEmpty()) return - private val javacExec: File - get() = getTool("javac") + if (missingTools.size == 1) jdkDistributionProbingError("${missingTools.single()} is missing") - private fun getTool(toolName: String): File { - val javaHomeBin = File(javaHome.get()).resolve("bin") - val tool = javaHomeBin.resolve(executableName(toolName)) - check(tool.exists()) { "Could not find $tool at: ${tool.absolutePath}}" } - return tool + jdkDistributionProbingError("${missingTools.joinToString(", ")} are missing") + } + + private fun jdkDistributionProbingError(errorMessage: String): Nothing { + val fullErrorMessage = buildString { + appendLine("Failed to check JDK distribution: $errorMessage") + appendLine("JDK distribution path: ${jdkHomeFile.absolutePath}") + } + error(fullErrorMessage) } @TaskAction fun run() { taskDir.ioFile.mkdirs() - val javaRuntimeVersion = try { - getJavaRuntimeVersionUnsafe()?.toIntOrNull() ?: -1 - } catch (e: Exception) { - throw IllegalStateException( - "Could not infer Java runtime version for Java home directory: ${javaHome.get()}", e - ) - } + val jdkHome = jdkHomeFile + val javaExecutable = jdkHome.getJdkTool("java") + val jlinkExecutable = jdkHome.getJdkTool("jlink") + val jpackageExecutabke = jdkHome.getJdkTool("jpackage") + ensureToolsExist(javaExecutable, jlinkExecutable, jpackageExecutabke) + + val jvmRuntimeVersionString = getJavaRuntimeVersion(javaExecutable) - check(javaRuntimeVersion >= MIN_JAVA_RUNTIME_VERSION) { - """|Packaging native distributions requires JDK runtime version >= $MIN_JAVA_RUNTIME_VERSION - |Actual version: '${javaRuntimeVersion ?: ""}' - |Java home: ${javaHome.get()} - """.trimMargin() + val jvmRuntimeVersion = jvmRuntimeVersionString?.toIntOrNull() + ?: jdkDistributionProbingError("JDK version '$jvmRuntimeVersionString' has unexpected format") + + check(jvmRuntimeVersion >= MIN_JAVA_RUNTIME_VERSION) { + jdkDistributionProbingError( + "minimum required JDK version is '$MIN_JAVA_RUNTIME_VERSION', " + + "but actual version is '$jvmRuntimeVersion'" + ) } val modules = arrayListOf() runExternalTool( - tool = javaExec, + tool = javaExecutable, args = listOf("--list-modules"), logToConsole = ExternalToolRunner.LogToConsole.Never, processStdout = { stdout -> @@ -83,67 +96,16 @@ abstract class AbstractCheckNativeDistributionRuntime : AbstractComposeDesktopTa } ) - val properties = JvmRuntimeProperties(javaRuntimeVersion, modules) + val properties = JvmRuntimeProperties(jvmRuntimeVersion, modules) JvmRuntimeProperties.writeToFile(properties, javaRuntimePropertiesFile.ioFile) } - private fun getJavaRuntimeVersionUnsafe(): String? { - fileOperations.clearDirs(workingDir) - val workingDir = workingDir.ioFile - - val printJavaRuntimeClassName = "PrintJavaRuntimeVersion" - val javaVersionPrefix = "Java runtime version = '" - val javaVersionSuffix = "'" - val printJavaRuntimeJava = workingDir.resolve("java/$printJavaRuntimeClassName.java").apply { - parentFile.mkdirs() - writeText(""" - import java.lang.reflect.Method; - - public class $printJavaRuntimeClassName { - public static void main(String[] args) { - Class runtimeClass = Runtime.class; - try { - Method version = runtimeClass.getMethod("version"); - Object runtimeVer = version.invoke(runtimeClass); - Class runtimeVerClass = runtimeVer.getClass(); - try { - int feature = (int) runtimeVerClass.getMethod("feature").invoke(runtimeVer); - printVersionAndHalt((Integer.valueOf(feature)).toString()); - } catch (NoSuchMethodException e) { - int major = (int) runtimeVerClass.getMethod("major").invoke(runtimeVer); - printVersionAndHalt((Integer.valueOf(major)).toString()); - } - } catch (Exception e) { - printVersionAndHalt(System.getProperty("java.version")); - } - } - - private static void printVersionAndHalt(String version) { - System.out.println("$javaVersionPrefix" + version + "$javaVersionSuffix"); - Runtime.getRuntime().exit(0); - } - } - """.trimIndent()) - } - val classFilesDir = workingDir.resolve("out-classes") - runExternalTool( - tool = javacExec, - args = listOf( - "-source", "1.8", - "-target", "1.8", - "-d", classFilesDir.absolutePath, - printJavaRuntimeJava.absolutePath - ) - ) - + private fun getJavaRuntimeVersion(javaExecutable: File): String? { var javaRuntimeVersion: String? = null runExternalTool( - tool = javaExec, - args = listOf("-cp", classFilesDir.absolutePath, printJavaRuntimeClassName), - processStdout = { stdout -> - val m = "$javaVersionPrefix(.+)$javaVersionSuffix".toRegex().find(stdout) - javaRuntimeVersion = m?.groupValues?.get(1) - } + tool = javaExecutable, + args = listOf("-jar", jdkVersionProbeJar.files.single().absolutePath), + processStdout = { stdout -> javaRuntimeVersion = stdout.trim() } ) return javaRuntimeVersion } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt index 013d101e58..b3257dc74f 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/tasks/AbstractConfigureDesktopPreviewTask.kt @@ -6,12 +6,12 @@ import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* -import org.jetbrains.compose.ComposeBuildConfig +import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.* +import org.jetbrains.compose.internal.utils.* import org.jetbrains.compose.internal.utils.currentTarget import org.jetbrains.compose.internal.utils.javaExecutable import org.jetbrains.compose.internal.utils.notNullProperty -import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask -import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.* import java.io.File abstract class AbstractConfigureDesktopPreviewTask : AbstractComposeDesktopTask() { @@ -37,14 +37,15 @@ abstract class AbstractConfigureDesktopPreviewTask : AbstractComposeDesktopTask( project.providers.gradleProperty("compose.desktop.preview.ide.port") @get:InputFiles - internal val uiTooling: FileCollection = project.configurations.detachedConfiguration( - project.dependencies.create("org.jetbrains.compose.ui:ui-tooling-desktop:${ComposeBuildConfig.composeVersion}") - ).apply { isTransitive = false } + internal val uiTooling: FileCollection = + project.detachedComposeDependency( + groupId = "org.jetbrains.compose.ui", + artifactId = "ui-tooling-desktop", + ).excludeTransitiveDependencies() @get:InputFiles - internal val hostClasspath: FileCollection = project.configurations.detachedConfiguration( - project.dependencies.create("org.jetbrains.compose:preview-rpc:${ComposeBuildConfig.composeVersion}") - ) + internal val hostClasspath: FileCollection = + project.detachedComposeGradleDependency(artifactId = "preview-rpc") @TaskAction fun run() { @@ -95,10 +96,12 @@ abstract class AbstractConfigureDesktopPreviewTask : AbstractComposeDesktopTask( } if (hasSkikoJvmRuntime) return emptyList() - if (hasSkikoJvm && skikoVersion != null && skikoVersion.isNotBlank()) { - val skikoRuntimeConfig = project.configurations.detachedConfiguration( - project.dependencies.create("org.jetbrains.skiko:skiko-awt-runtime-${currentTarget.id}:$skikoVersion") - ).apply { isTransitive = false } + if (hasSkikoJvm && !skikoVersion.isNullOrBlank()) { + val skikoRuntimeConfig = project.detachedDependency( + groupId = "org.jetbrains.skiko", + artifactId = "skiko-awt-runtime-${currentTarget.id}", + version = skikoVersion + ).excludeTransitiveDependencies() return skikoRuntimeConfig.files } } catch (e: Exception) { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/web/internal/configureExperimentalWebApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/web/internal/configureExperimentalWebApplication.kt index a110831933..fd82075cc7 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/web/internal/configureExperimentalWebApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/experimental/web/internal/configureExperimentalWebApplication.kt @@ -12,8 +12,9 @@ import org.gradle.api.artifacts.UnresolvedDependency import org.gradle.api.provider.Provider import org.jetbrains.compose.ComposeBuildConfig import org.jetbrains.compose.experimental.dsl.ExperimentalWebApplication -import org.jetbrains.compose.internal.utils.registerTask import org.jetbrains.compose.experimental.web.tasks.ExperimentalUnpackSkikoWasmRuntimeTask +import org.jetbrains.compose.internal.utils.* +import org.jetbrains.compose.internal.utils.registerTask import org.jetbrains.compose.internal.utils.uppercaseFirstChar import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget @@ -43,8 +44,9 @@ private const val SKIKO_GROUP = "org.jetbrains.skiko" private fun skikoVersionProvider(project: Project): Provider { val composeVersion = ComposeBuildConfig.composeVersion - val configurationWithSkiko = project.configurations.detachedConfiguration( - project.dependencies.create("org.jetbrains.compose.ui:ui-graphics:$composeVersion") + val configurationWithSkiko = project.detachedComposeDependency( + artifactId = "ui-graphics", + groupId = "org.jetbrains.compose.ui" ) return project.provider { val skikoDependency = configurationWithSkiko.allDependenciesDescriptors.firstOrNull(::isSkikoDependency) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt index 966c05a9e0..570262ba67 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/gradleUtils.kt @@ -6,7 +6,9 @@ package org.jetbrains.compose.internal.utils import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration import org.gradle.api.logging.Logger +import org.jetbrains.compose.ComposeBuildConfig import java.util.* internal inline fun Logger.info(fn: () -> String) { @@ -35,3 +37,27 @@ fun Project.getLocalProperty(key: String): String? { return null } } + +internal fun Project.detachedComposeGradleDependency( + artifactId: String, + groupId: String = "org.jetbrains.compose", +): Configuration = + detachedDependency(groupId = groupId, artifactId = artifactId, version = ComposeBuildConfig.composeGradlePluginVersion) + +internal fun Project.detachedComposeDependency( + artifactId: String, + groupId: String = "org.jetbrains.compose", +): Configuration = + detachedDependency(groupId = groupId, artifactId = artifactId, version = ComposeBuildConfig.composeVersion) + +internal fun Project.detachedDependency( + groupId: String, + artifactId: String, + version: String +): Configuration = + project.configurations.detachedConfiguration( + project.dependencies.create("$groupId:$artifactId:$version") + ) + +internal fun Configuration.excludeTransitiveDependencies(): Configuration = + apply { isTransitive = false } diff --git a/gradle-plugins/jdk-version-probe/build.gradle.kts b/gradle-plugins/jdk-version-probe/build.gradle.kts new file mode 100644 index 0000000000..0dd1653676 --- /dev/null +++ b/gradle-plugins/jdk-version-probe/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + java + id("maven-publish") +} + +mavenPublicationConfig { + displayName = "JDK version probe" + description = "JDK version probe (Internal)" + artifactId = "gradle-plugin-internal-jdk-version-probe" +} + +tasks.jar.configure { + manifest.attributes["Main-Class"] = "org.jetbrains.compose.desktop.application.internal.JdkVersionProbe" +} \ No newline at end of file diff --git a/gradle-plugins/jdk-version-probe/src/main/java/org/jetbrains/compose/desktop/application/internal/JdkVersionProbe.java b/gradle-plugins/jdk-version-probe/src/main/java/org/jetbrains/compose/desktop/application/internal/JdkVersionProbe.java new file mode 100644 index 0000000000..35edb75f9d --- /dev/null +++ b/gradle-plugins/jdk-version-probe/src/main/java/org/jetbrains/compose/desktop/application/internal/JdkVersionProbe.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2023 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ +package org.jetbrains.compose.desktop.application.internal; + +import java.lang.reflect.Method; + +public class JdkVersionProbe { + public static void main(String[] args) { + Class runtimeClass = Runtime.class; + try { + Method version = runtimeClass.getMethod("version"); + Object runtimeVer = version.invoke(runtimeClass); + Class runtimeVerClass = runtimeVer.getClass(); + try { + int feature = (int) runtimeVerClass.getMethod("feature").invoke(runtimeVer); + printVersionAndHalt((Integer.valueOf(feature)).toString()); + } catch (NoSuchMethodException e) { + int major = (int) runtimeVerClass.getMethod("major").invoke(runtimeVer); + printVersionAndHalt((Integer.valueOf(major)).toString()); + } + } catch (Exception e) { + String javaVersion = System.getProperty("java.version"); + String[] parts = javaVersion.split("\\."); + if (parts.length > 2 && "1".equalsIgnoreCase(parts[0])) { + printVersionAndHalt(parts[1]); + } else { + throw new IllegalStateException("Could not determine JDK version from string: '" + javaVersion + "'"); + } + } + } + + private static void printVersionAndHalt(String version) { + System.out.println(version); + Runtime.getRuntime().exit(0); + } +} \ No newline at end of file diff --git a/gradle-plugins/settings.gradle.kts b/gradle-plugins/settings.gradle.kts index 701e7efb0d..162038bf7d 100644 --- a/gradle-plugins/settings.gradle.kts +++ b/gradle-plugins/settings.gradle.kts @@ -7,3 +7,4 @@ pluginManagement { include(":compose") include(":preview-rpc") +include(":jdk-version-probe")