Skip to content

Commit

Permalink
Make workflow generation via Gradle able to run in parallel
Browse files Browse the repository at this point in the history
  • Loading branch information
Vampire committed Jan 31, 2025
1 parent c389573 commit a408856
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 82 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020-2024 Björn Kautler
* Copyright 2020-2025 Björn Kautler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,26 +16,18 @@

package net.kautler

import net.kautler.githubactions.DetermineImportedFiles
import net.kautler.githubactions.PreprocessGithubWorkflow
import org.gradle.accessors.dm.LibrariesForLibs
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY
import org.jetbrains.kotlin.cli.common.messages.MessageCollector.Companion.NONE
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles.JVM_CONFIG_FILES
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.com.intellij.openapi.vfs.local.CoreLocalFileSystem
import org.jetbrains.kotlin.com.intellij.openapi.vfs.local.CoreLocalVirtualFile
import org.jetbrains.kotlin.com.intellij.psi.PsiManager
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import java.nio.file.Path

plugins {
`java-base`
}

val compilerEmbeddableClasspath by configurations.creating {
isCanBeConsumed = false
}

val compilerClasspath by configurations.creating {
isCanBeConsumed = false
}
Expand All @@ -47,6 +39,7 @@ val scriptClasspath by configurations.creating {
val libs = the<LibrariesForLibs>()

dependencies {
compilerEmbeddableClasspath(libs.workflows.kotlin.compiler.embeddable)
compilerClasspath(libs.workflows.kotlin.compiler)
compilerClasspath(libs.workflows.kotlin.scripting.compiler)
scriptClasspath(libs.workflows.kotlin.main.kts) {
Expand All @@ -64,72 +57,23 @@ file(".github/workflows")
val pascalCasedWorkflowName = workflowName.replace("""-\w""".toRegex()) {
it.value.substring(1).replaceFirstChar(Char::uppercaseChar)
}.replaceFirstChar(Char::uppercaseChar)
val preprocessWorkflow = tasks.register<JavaExec>("preprocess${pascalCasedWorkflowName}Workflow") {
group = "github actions"

inputs
.file(workflowScript)
.withPropertyName("workflowScript")
inputs
.files(file(workflowScript).importedFiles)
.withPropertyName("importedFiles")
outputs
.file(workflowScript.resolveSibling("$workflowName.yaml"))
.withPropertyName("workflowFile")

javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
})
classpath(compilerClasspath)
mainClass.set(K2JVMCompiler::class.qualifiedName)
args("-no-stdlib", "-no-reflect")
args("-classpath", scriptClasspath.asPath)
args("-script", workflowScript.absolutePath)

// work-around for https://youtrack.jetbrains.com/issue/KT-42101
systemProperty("kotlin.main.kts.compiled.scripts.cache.dir", "")
}
val determineImportedFiles =
tasks.register<DetermineImportedFiles>("determineImportedFilesFor${pascalCasedWorkflowName}Workflow") {
mainKtsFile.set(workflowScript)
importedFiles.set(layout.buildDirectory.file("importedFilesFor${pascalCasedWorkflowName}Workflow.txt"))
kotlinCompilerEmbeddableClasspath.from(compilerEmbeddableClasspath)
}
val preprocessWorkflow =
tasks.register<PreprocessGithubWorkflow>("preprocess${pascalCasedWorkflowName}Workflow") {
this.workflowScript.set(workflowScript)
importedFiles.from(determineImportedFiles.flatMap { it.importedFiles }.map { it.asFile.readLines() })
kotlinCompilerClasspath.from(compilerClasspath)
mainKtsClasspath.from(scriptClasspath)
javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
})
}
preprocessWorkflows {
dependsOn(preprocessWorkflow)
}
}

val File.importedFiles: List<File>
get() = if (!isFile) {
emptyList()
} else {
PsiManager
.getInstance(
KotlinCoreEnvironment
.createForProduction(
Disposer.newDisposable(),
CompilerConfiguration().apply {
put(MESSAGE_COLLECTOR_KEY, NONE)
},
JVM_CONFIG_FILES
)
.project
)
.findFile(
// work-around for API change between version we compile against and version we run against
// after upgrading Gradle to a version that contains Kotlin 1.9 the embeddable compiler can
// be upgraded to v2 also for compilation and then this can be removed
CoreLocalVirtualFile::class
.java
.getConstructor(CoreLocalFileSystem::class.java, Path::class.java)
.newInstance(CoreLocalFileSystem(), toPath())
)
.let { it as KtFile }
.fileAnnotationList
?.annotationEntries
?.asSequence()
?.filter { it.shortName?.asString() == "Import" }
?.flatMap { it.valueArgumentList?.arguments ?: emptyList() }
?.mapNotNull { it.getArgumentExpression() as? KtStringTemplateExpression }
?.map { it.entries.first() }
?.mapNotNull { it as? KtLiteralStringTemplateEntry }
?.map { resolveSibling(it.text) }
?.flatMap { it.importedFiles + it }
?.toList()
?: emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2025 Björn Kautler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.kautler.githubactions

import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.ProjectLayout
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.UntrackedTask
import org.gradle.kotlin.dsl.submit
import org.gradle.workers.WorkerExecutor
import javax.inject.Inject

@UntrackedTask(because = "imported files can import other files so inputs are not determinable upfront")
abstract class DetermineImportedFiles : DefaultTask() {
@get:InputFile
abstract val mainKtsFile: RegularFileProperty

@get:InputFiles
abstract val kotlinCompilerEmbeddableClasspath: ConfigurableFileCollection

@get:OutputFile
abstract val importedFiles: RegularFileProperty

@get:Inject
abstract val workerExecutor: WorkerExecutor

@get:Inject
abstract val layout: ProjectLayout

@TaskAction
fun determineImportedFiles() {
workerExecutor.classLoaderIsolation {
classpath.from(kotlinCompilerEmbeddableClasspath)
}.submit(DetermineImportedFilesWorkAction::class) {
projectDirectory.set(layout.projectDirectory)
mainKtsFile.set(this@DetermineImportedFiles.mainKtsFile)
importedFiles.set(this@DetermineImportedFiles.importedFiles)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2025 Björn Kautler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.kautler.githubactions

import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY
import org.jetbrains.kotlin.cli.common.messages.MessageCollector.Companion.NONE
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles.JVM_CONFIG_FILES
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.com.intellij.openapi.vfs.local.CoreLocalFileSystem
import org.jetbrains.kotlin.com.intellij.openapi.vfs.local.CoreLocalVirtualFile
import org.jetbrains.kotlin.com.intellij.psi.PsiManager
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import java.io.File
import java.nio.file.Path

abstract class DetermineImportedFilesWorkAction : WorkAction<DetermineImportedFilesWorkAction.Parameters> {
override fun execute() {
val projectDirectory = parameters.projectDirectory.get().asFile
parameters
.mainKtsFile
.get()
.asFile
.importedFiles
.map { it.relativeTo(projectDirectory).invariantSeparatorsPath }
.distinct()
.sorted()
.joinToString("\n")
.also(parameters.importedFiles.get().asFile::writeText)
}

interface Parameters : WorkParameters {
val projectDirectory: DirectoryProperty
val mainKtsFile: RegularFileProperty
val importedFiles: RegularFileProperty
}
}

private val File.importedFiles: List<File>
get() = if (!isFile) {
emptyList()
} else {
PsiManager
.getInstance(
KotlinCoreEnvironment
.createForProduction(
Disposer.newDisposable(),
CompilerConfiguration().apply {
put(MESSAGE_COLLECTOR_KEY, NONE)
},
JVM_CONFIG_FILES
)
.project
)
.findFile(CoreLocalVirtualFile::class.java.getConstructor(CoreLocalFileSystem::class.java, Path::class.java).newInstance(CoreLocalFileSystem(), toPath()))
.let { it as KtFile }
.fileAnnotationList
?.annotationEntries
?.asSequence()
?.filter { it.shortName?.asString() == "Import" }
?.flatMap { it.valueArgumentList?.arguments ?: emptyList() }
?.mapNotNull { it.getArgumentExpression() as? KtStringTemplateExpression }
?.map { it.entries.first() }
?.mapNotNull { it as? KtLiteralStringTemplateEntry }
?.map { resolveSibling(it.text) }
?.flatMap { it.importedFiles + it }
?.toList()
?: emptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2025 Björn Kautler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.kautler.githubactions

import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.kotlin.dsl.submit
import org.gradle.workers.WorkerExecutor
import java.io.File
import javax.inject.Inject

abstract class PreprocessGithubWorkflow : DefaultTask() {
@get:InputFile
abstract val workflowScript: RegularFileProperty

@get:InputFiles
abstract val importedFiles: ConfigurableFileCollection

@get:InputFiles
abstract val kotlinCompilerClasspath: ConfigurableFileCollection

@get:InputFiles
abstract val mainKtsClasspath: ConfigurableFileCollection

@get:Nested
abstract val javaLauncher: Property<JavaLauncher>

@get:OutputFile
val workflowFile: Provider<File> = workflowScript.map {
val workflowScript = it.asFile
workflowScript.resolveSibling("${workflowScript.name.removeSuffix(".main.kts")}.yaml")
}

@get:Inject
abstract val workerExecutor: WorkerExecutor

init {
group = "github workflows"
}

@TaskAction
fun determineImportedFiles() {
workerExecutor.noIsolation().submit(PreprocessGithubWorkflowWorkAction::class) {
workflowScript.set(this@PreprocessGithubWorkflow.workflowScript)
kotlinCompilerClasspath.from(this@PreprocessGithubWorkflow.kotlinCompilerClasspath)
mainKtsClasspath.from(this@PreprocessGithubWorkflow.mainKtsClasspath)
javaExecutable.set(this@PreprocessGithubWorkflow.javaLauncher.map { it.executablePath })
}
}
}
Loading

0 comments on commit a408856

Please sign in to comment.