From 1017302e0ff44e2142885bc3ec614eb7f75fca97 Mon Sep 17 00:00:00 2001 From: Joffrey Bion Date: Fri, 13 Nov 2020 18:37:01 +0100 Subject: [PATCH] [space] SPACE-12112 Migrate automation to idea-specific forward-compatible API IJ-MR-4336 GitOrigin-RevId: 0e500207c8e4cca11c7596cf97ea14da70bb3d34 --- .idea/libraries/space_idea_sdk.xml | 4 +- build/dependencies/setupSpacePlugin.gradle | 2 +- .../services/ScriptKtsPersistentState.kt | 16 ++- .../services/SpaceKtsModelBuilder.kt | 123 +++++++++--------- .../pipelines/ui/SpaceScriptsViewFactory.kt | 120 +++++++++++------ .../pipelines/viewmodel/ScriptModel.kt | 5 +- 6 files changed, 154 insertions(+), 116 deletions(-) diff --git a/.idea/libraries/space_idea_sdk.xml b/.idea/libraries/space_idea_sdk.xml index 9a4e17fe6cca..e3fe1ad14d9f 100644 --- a/.idea/libraries/space_idea_sdk.xml +++ b/.idea/libraries/space_idea_sdk.xml @@ -1,6 +1,6 @@ - + @@ -32,7 +32,7 @@ - + diff --git a/build/dependencies/setupSpacePlugin.gradle b/build/dependencies/setupSpacePlugin.gradle index 324712486b85..9de6a995d1da 100644 --- a/build/dependencies/setupSpacePlugin.gradle +++ b/build/dependencies/setupSpacePlugin.gradle @@ -2,7 +2,7 @@ import java.util.concurrent.TimeUnit task setupSpaceAutomationDefinitions { - def version = "1.0.59855" + def version = "1.0.60200" def artifact = file("$buildDir/space/space-idea-script-definition.jar") outputs.file(artifact) doLast { diff --git a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/ScriptKtsPersistentState.kt b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/ScriptKtsPersistentState.kt index c9e3db8985c7..82ff5acda065 100644 --- a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/ScriptKtsPersistentState.kt +++ b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/ScriptKtsPersistentState.kt @@ -1,9 +1,10 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.space.plugins.pipelines.services -import circlet.pipelines.config.api.ScriptConfig +import circlet.automation.bootstrap.AutomationDslEvaluationBootstrap import circlet.pipelines.config.api.parseProjectConfig -import circlet.pipelines.config.api.printJson +import circlet.pipelines.config.idea.api.IdeaScriptConfig +import circlet.pipelines.config.utils.AutomationCompilerConfiguration import com.intellij.openapi.project.Project import com.intellij.openapi.project.getProjectCachePath import com.intellij.util.io.safeOutputStream @@ -20,7 +21,7 @@ private val log = logger() class ScriptKtsPersistentState(val project: Project) { // not for load failure - fun load(): ScriptConfig? { + fun load(): IdeaScriptConfig? { val path = getCacheFile(project) @@ -37,7 +38,12 @@ class ScriptKtsPersistentState(val project: Project) { return try { Files.newBufferedReader(path, Charsets.UTF_8).use { reader -> - reader.readLine().parseProjectConfig() + val evalService = AutomationDslEvaluationBootstrap(log, automationConfiguration()).loadEvaluatorForIdea() + if (evalService == null) { + log.error("DSL evaluation service not found, cannot deserialize automation DSL model") + return null + } + evalService.deserializeJsonConfig(reader.readLine()) } } catch (e: NoSuchFileException) { @@ -53,7 +59,7 @@ class ScriptKtsPersistentState(val project: Project) { return project.getProjectCachePath("space_automation").resolve(".space.kts.dat") } - fun save(config: ScriptConfig) { + fun save(config: IdeaScriptConfig) { val path = getCacheFile(project) path.safeOutputStream().use { it.write(config.printJson().toByteArray(Charsets.UTF_8)) diff --git a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/SpaceKtsModelBuilder.kt b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/SpaceKtsModelBuilder.kt index 9eb0f28c5e48..5304c0fb2494 100644 --- a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/SpaceKtsModelBuilder.kt +++ b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/services/SpaceKtsModelBuilder.kt @@ -2,12 +2,10 @@ package com.intellij.space.plugins.pipelines.services import circlet.automation.bootstrap.AutomationCompilerBootstrap +import circlet.automation.bootstrap.AutomationDslEvaluationBootstrap import circlet.automation.bootstrap.embeddedMavenServer import circlet.automation.bootstrap.publicMavenServer -import circlet.pipelines.config.api.ScriptConfig -import circlet.pipelines.config.dsl.script.exec.common.ProjectConfigValidationResult -import circlet.pipelines.config.dsl.script.exec.common.evaluateModel -import circlet.pipelines.config.dsl.script.exec.common.validate +import circlet.pipelines.config.idea.api.IdeaScriptConfig import circlet.pipelines.config.utils.AutomationCompilerConfiguration import circlet.platform.client.backgroundDispatcher import com.intellij.openapi.components.Service @@ -35,9 +33,8 @@ import org.slf4j.event.SubstituteLoggingEvent import org.slf4j.helpers.SubstituteLogger import runtime.Ui import runtime.reactive.* -import java.io.File -import java.io.PrintWriter -import java.io.StringWriter +import java.io.* +import java.nio.file.Files import java.nio.file.Paths private val log = logger() @@ -86,7 +83,7 @@ class SpaceKtsModelBuilder(val project: Project) : LifetimedDisposable by Lifeti private inner class ScriptModelHolder(val lifetime: Lifetime, val scriptFile: VirtualFile, modelBuildIsRequested: Property, - loadedConfig: ScriptConfig?) : ScriptModel { + loadedConfig: IdeaScriptConfig?) : ScriptModel { val sync = Any() @@ -94,11 +91,11 @@ class SpaceKtsModelBuilder(val project: Project) : LifetimedDisposable by Lifeti private val _error = mutableProperty(null) private val _state = mutableProperty(if (loadedConfig == null) ScriptState.NotInitialised else ScriptState.Ready) - override val config: Property get() = _config + override val config: Property get() = _config override val error: Property get() = _error override val state: Property get() = _state - val logBuildData = mutableProperty(null) + private val logBuildData = mutableProperty(null) init { @@ -144,86 +141,56 @@ class SpaceKtsModelBuilder(val project: Project) : LifetimedDisposable by Lifeti private fun build(logData: LogData) { lifetime.using { lt -> - val events = ObservableQueue.mutable() - - events.change.forEach(lt) { - val ev = it.index - when (ev.level) { - Level.INFO -> { - logData.message(ev.message) - } - Level.DEBUG -> { - logData.message(ev.message) - } - Level.WARN -> { - logData.message(ev.message) - } - Level.ERROR -> { - logData.error(ev.message) - } - Level.TRACE -> { - } - } - } - - val eventLogger = KLogger( - JVMLogger( - SubstituteLogger("ScriptModelBuilderLogger", events, false) - ) - ) + val eventLogger = logData.asLogger(lt) + val outputDir = createTempDir("space-automation-temp") try { - val tempDir = createTempDir() + val outputDirPath = outputDir.toPath() + val compiledJarPath = outputDirPath.resolve("compiledJar.jar") + val scriptRuntimePath = outputDirPath.resolve("space-automation-runtime.jar") - val outputFolder = tempDir.absolutePath + "/" - val jarFile = File(outputFolder, "compiledJar.jar") + val configuration = automationConfiguration() + val compiler = AutomationCompilerBootstrap(log = eventLogger, configuration = configuration) - // Primary option is to download from currently connected server, fallback on the public maven - val server = SpaceWorkspaceComponent.getInstance().workspace.value?.client?.server?.let { embeddedMavenServer(it) } ?: publicMavenServer + val compileResultCode = compiler.compile(script = Paths.get(scriptFile.path), jar = compiledJarPath) - val configuration = AutomationCompilerConfiguration.Remote(server = server) - - val compile = AutomationCompilerBootstrap(eventLogger, configuration = configuration).compile( - Paths.get(scriptFile.path), - jarFile.toPath() - ) - - if (compile != 0) { + if (compileResultCode != 0) { _config.value = null - _error.value = "Compilation failed, $compile" + _error.value = "Compilation failed, $compileResultCode" return@using } - if (!jarFile.exists() || !jarFile.isFile) { + if (!Files.isRegularFile(compiledJarPath)) { _config.value = null - _error.value = "Compilation failed: can't find output file ${jarFile.absolutePath}" + _error.value = "Compilation failed: can't find output file ${compiledJarPath}" return@using } - val scriptRuntimePath = "$tempDir/space-automation-runtime.jar" - // See AutomationCompiler.kt's where we copy the runtime jar into the output folder for all resolver types - if (!File(scriptRuntimePath).exists()) { + // See AutomationCompiler.kt (in space project) where we copy the runtime jar into the output folder for all resolver types + if (!Files.exists(scriptRuntimePath)) { _config.value = null _error.value = "script-automation-runtime.jar is missing after script compilation." return@using } - val config = evaluateModel(jarFile.absolutePath, scriptRuntimePath) - val scriptConfig = config.config() + val evaluator = AutomationDslEvaluationBootstrap(log = eventLogger, configuration = configuration).loadEvaluatorForIdea() + if (evaluator == null) { + _config.value = null + _error.value = "DSL evaluation service not found." + return@using + } + val evalResult = evaluator.evaluateAndValidate(compiledJarPath, scriptRuntimePath) - val validationResult = scriptConfig.validate() - - if (validationResult is ProjectConfigValidationResult.Failed) { - val message = validationResult.errorMessage() // NON-NLS + if (evalResult.validationErrors.any()) { + val message = evalResult.validationErrors.joinToString("\n", prefix = "Validation errors:\n") logData.error(message) _config.value = null _error.value = message return@using } + _config.value = evalResult.config _error.value = null - _config.value = scriptConfig - } catch (th: Throwable) { val errors = StringWriter() @@ -233,7 +200,35 @@ class SpaceKtsModelBuilder(val project: Project) : LifetimedDisposable by Lifeti // do not touch last config, just update the error state. _error.value = errors.toString() } + finally { + outputDir.deleteRecursively() + } } } } } + +internal fun automationConfiguration(): AutomationCompilerConfiguration { + val spaceClient = SpaceWorkspaceComponent.getInstance().workspace.value?.client + // Primary option is to download from currently connected server, fallback to the public maven + val server = spaceClient?.server?.let { embeddedMavenServer(it) } ?: publicMavenServer + return AutomationCompilerConfiguration.Remote(server = server) +} + +private fun LogData.asLogger(lifetime: Lifetime): KLogger { + val queue = ObservableQueue.mutable() + + queue.change.forEach(lifetime) { + val ev = it.index + when (ev.level) { + Level.INFO -> message(ev.message) + Level.DEBUG -> message(ev.message) + Level.WARN -> message(ev.message) + Level.ERROR -> error(ev.message) + Level.TRACE -> { + } + } + } + // slf4j's SubstituteLogger mechanism allows us to put logs as events in a queue (even though not initially made for this) + return KLogger(JVMLogger(SubstituteLogger("ScriptModelBuilderLogger", queue, false))) +} diff --git a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/ui/SpaceScriptsViewFactory.kt b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/ui/SpaceScriptsViewFactory.kt index 62e029cffb15..6b911d3520bf 100644 --- a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/ui/SpaceScriptsViewFactory.kt +++ b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/ui/SpaceScriptsViewFactory.kt @@ -2,10 +2,9 @@ package com.intellij.space.plugins.pipelines.ui import circlet.pipelines.DefaultDslFileName -import circlet.pipelines.config.api.ScriptConfig -import circlet.pipelines.config.api.ScriptStep -import circlet.pipelines.config.api.ScriptStep.* -import circlet.pipelines.config.api.ScriptStep.ProcessExecutable.* +import circlet.pipelines.config.idea.api.* +import circlet.pipelines.config.idea.api.ScriptStep.* +import circlet.pipelines.config.idea.api.ProcessExecutable.* import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil import com.intellij.ide.IdeBundle @@ -50,6 +49,9 @@ import javax.swing.JPanel import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultTreeModel import javax.swing.tree.TreeSelectionModel +import kotlin.reflect.full.isSubtypeOf +import kotlin.reflect.jvm.javaType +import kotlin.reflect.typeOf class SpaceToolWindowViewModel(val lifetime: Lifetime) { val taskIsRunning = mutableProperty(false) @@ -221,7 +223,7 @@ class SpaceToolWindowService(val project: Project) : LifetimedDisposable by Life } private fun resetNodes(root: DefaultMutableTreeNode, - config: ScriptConfig?, + config: IdeaScriptConfig?, error: String?, state: ScriptState, extendedViewModeEnabled: Boolean) { @@ -238,12 +240,12 @@ class SpaceToolWindowService(val project: Project) : LifetimedDisposable by Life return } - val tasks = config.jobs + val jobs = config.jobs val targets = config.targets val pipelines = config.pipelines var jobsTypesCount = 0 - if (tasks.any()) { + if (jobs.any()) { jobsTypesCount++ } if (targets.any()) { @@ -253,29 +255,11 @@ class SpaceToolWindowService(val project: Project) : LifetimedDisposable by Life jobsTypesCount++ } val shouldAddGroupingNodes = jobsTypesCount > 1 - if (tasks.any()) { - val tasksCollectionNode = getGroupingNode(root, "tasks", shouldAddGroupingNodes) - config.jobs.forEach { - val taskNode = SpaceModelTreeNode(it.name, true) - if (extendedViewModeEnabled) { - val triggers = it.triggers - if (triggers.any()) { - val triggersNode = SpaceModelTreeNode("triggers") - triggers.forEach { trigger -> - triggersNode.add(SpaceModelTreeNode(trigger::class.java.simpleName)) - } - - taskNode.add(triggersNode) - } - - val jobsNode = SpaceModelTreeNode("jobs") - it.steps.forEach { step -> - jobsNode.add(step.toTreeNode()) - } - taskNode.add(jobsNode) - } - - tasksCollectionNode.add(taskNode) + if (jobs.any()) { + val jobsCollectionNode = getGroupingNode(root, "jobs", shouldAddGroupingNodes) + config.jobs.forEach { job -> + val jobNode = job.toTreeNode(showChildren = extendedViewModeEnabled) + jobsCollectionNode.add(jobNode) } } @@ -350,22 +334,52 @@ class SpaceToolWindowService(val project: Project) : LifetimedDisposable by Life } } -private fun ScriptStep.toTreeNode(): SpaceModelTreeNode = when (val job = this) { - is CompositeStep -> SpaceModelTreeNode(job::class.java.simpleName).apply { - job.children.forEach { child -> - add(child.toTreeNode()) +private fun ScriptJob.toTreeNode(showChildren: Boolean) = SpaceModelTreeNode(name, true).apply { + if (showChildren) { + if (triggers.any()) { + add(triggers.toTreeNode()) } + add(steps.toTreeNode()) } - is SimpleStep.Process.Container -> SpaceModelTreeNode("container: ${job.image}").apply { - add(job.data.exec.toTreeNode()) - } - is SimpleStep.Process.VM -> SpaceModelTreeNode("vm: ${job.image}") - is SimpleStep.DockerComposeStep -> SpaceModelTreeNode("compose: ${job.mainService}") } -private fun ProcessExecutable<*>.toTreeNode() = DefaultMutableTreeNode(treeNodeText) +private fun List.toTreeNode() = SpaceModelTreeNode("triggers").apply { + forEach { trigger -> + add(trigger.toTreeNode()) + } +} -private val ProcessExecutable<*>.treeNodeText: String +private fun Trigger.toTreeNode() = SpaceModelTreeNode(usefulSubTypeName() ?: "other trigger") + +private fun StepSequence.toTreeNode() = SpaceModelTreeNode("steps").apply { + forEach { step -> + add(step.toTreeNode()) + } +} + +private fun ScriptStep.toTreeNode(): SpaceModelTreeNode = when (val step = this) { + is CompositeStep -> SpaceModelTreeNode(step.treeNodeText).apply { + step.children.forEach { subStep -> + add(subStep.toTreeNode()) + } + } + is SimpleStep.Process.Container -> SpaceModelTreeNode("container: ${step.image}").apply { + add(step.data.exec.toTreeNode()) + } + is SimpleStep.Process.VM -> SpaceModelTreeNode("vm: ${step.image}") + is SimpleStep.DockerComposeStep -> SpaceModelTreeNode("compose: ${step.mainService}") + else -> SpaceModelTreeNode(step::class.simpleName) +} + +private val CompositeStep.treeNodeText: String get() = when(this) { + is CompositeStep.Fork -> "parallel" + is CompositeStep.Sequence -> "sequence" + else -> usefulSubTypeName() ?: "composite step" +} + +private fun ProcessExecutable.toTreeNode() = DefaultMutableTreeNode(treeNodeText) + +private val ProcessExecutable.treeNodeText: String get() = when (this) { is ContainerExecutable.DefaultCommand -> "exec: defaultCommand${args.presentArgs()}" is ContainerExecutable.OverrideEntryPoint -> "exec: overrideEntryPoint: $entryPoint${args.presentArgs()}" @@ -373,8 +387,32 @@ private val ProcessExecutable<*>.treeNodeText: String is KotlinScript -> "exec: kts script" is ShellScript -> "exec: shell script" is VMExecutable -> "exec: VM executable" + else -> "exec: ${this::class.simpleName}" } private fun List.presentArgs(): String { return if (this.any()) ". args: ${this.joinToString()}" else "" } + +/** + * Returns the name of this object's dynamic type (which is a subtype of its static type [T]). + * If this object is of an anonymous type, this method falls back to the name of the closest supertype of the runtime type of this object + * that implements/extends its static type [T]. + * If this object is a direct anonymous implementation of the static type [T], this method returns `null` to allow other fallbacks. + * + * Most instances of the interfaces are anonymous classes at the moment in the space project. + * For instance, the [Trigger] interface may have subinterfaces that we don't know of at compile time here, but could be added in the + * future. + * Since the object is of an anonymous class, we should use the name of the closest supertype that implements [Trigger], e.g. GitPush. + */ +@OptIn(ExperimentalStdlibApi::class) +private inline fun T.usefulSubTypeName(): String? { + val simpleClassName = this::class.simpleName + if (simpleClassName != null) { // false for anonymous classes + return simpleClassName + } + // There is always at least one supertype (T), because we know this is an anonymous class that is also an instance of T + val subTypeOfT = this::class.supertypes.first { it.isSubtypeOf(typeOf()) } + val simpleSubtypeName = subTypeOfT.javaType.typeName.substringAfterLast('.') + return simpleSubtypeName.takeIf { it != T::class.simpleName } +} diff --git a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/viewmodel/ScriptModel.kt b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/viewmodel/ScriptModel.kt index bc2863b3a474..c406b1f6b558 100644 --- a/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/viewmodel/ScriptModel.kt +++ b/plugins/space/src/main/kotlin/com/intellij/space/plugins/pipelines/viewmodel/ScriptModel.kt @@ -1,7 +1,7 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.space.plugins.pipelines.viewmodel -import circlet.pipelines.config.api.ScriptConfig +import circlet.pipelines.config.idea.api.IdeaScriptConfig import com.intellij.build.events.BuildEvent import com.intellij.build.events.BuildEventsNls import com.intellij.build.events.impl.OutputBuildEventImpl @@ -12,11 +12,10 @@ import runtime.reactive.ObservableList import runtime.reactive.Property import javax.swing.tree.DefaultMutableTreeNode - enum class ScriptState { NotInitialised, Building, Ready } interface ScriptModel { - val config: Property + val config: Property val error: Property val state: Property }