[space] SPACE-12112 Migrate automation to idea-specific forward-compatible API

IJ-MR-4336

GitOrigin-RevId: 0e500207c8e4cca11c7596cf97ea14da70bb3d34
This commit is contained in:
Joffrey Bion
2020-11-13 18:37:01 +01:00
committed by intellij-monorepo-bot
parent 40d8d32c02
commit 1017302e0f
6 changed files with 154 additions and 116 deletions

View File

@@ -1,6 +1,6 @@
<component name="libraryTable">
<library name="space-idea-sdk" type="repository">
<properties maven-id="com.jetbrains:space-idea-sdk:1.1.59855">
<properties maven-id="com.jetbrains:space-idea-sdk:1.1.60200">
<exclude>
<dependency maven-id="org.jetbrains.kotlin:kotlin-stdlib" />
<dependency maven-id="org.jetbrains.kotlin:kotlin-reflect" />
@@ -32,7 +32,7 @@
</exclude>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/jetbrains/space-idea-sdk/1.1.59855/space-idea-sdk-1.1.59855.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/com/jetbrains/space-idea-sdk/1.1.60200/space-idea-sdk-1.1.60200.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/reactivex/rxkotlin/0.55.0/rxkotlin-0.55.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/io/reactivex/rxjava/1.1.1/rxjava-1.1.1.jar!/" />
</CLASSES>

View File

@@ -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 {

View File

@@ -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<ScriptKtsPersistentState>()
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))

View File

@@ -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<SpaceKtsModelBuilder>()
@@ -86,7 +83,7 @@ class SpaceKtsModelBuilder(val project: Project) : LifetimedDisposable by Lifeti
private inner class ScriptModelHolder(val lifetime: Lifetime,
val scriptFile: VirtualFile,
modelBuildIsRequested: Property<Boolean>,
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<String?>(null)
private val _state = mutableProperty(if (loadedConfig == null) ScriptState.NotInitialised else ScriptState.Ready)
override val config: Property<ScriptConfig?> get() = _config
override val config: Property<IdeaScriptConfig?> get() = _config
override val error: Property<String?> get() = _error
override val state: Property<ScriptState> get() = _state
val logBuildData = mutableProperty<LogData?>(null)
private val logBuildData = mutableProperty<LogData?>(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<SubstituteLoggingEvent>()
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<SubstituteLoggingEvent>()
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)))
}

View File

@@ -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<Trigger>.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<String>.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 <reified T : Any> 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<T>()) }
val simpleSubtypeName = subTypeOfT.javaType.typeName.substringAfterLast('.')
return simpleSubtypeName.takeIf { it != T::class.simpleName }
}

View File

@@ -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<ScriptConfig?>
val config: Property<IdeaScriptConfig?>
val error: Property<String?>
val state: Property<ScriptState>
}