From 13b18c5865d640a19e74af18b03b6d3d77031d35 Mon Sep 17 00:00:00 2001 From: Konstantin Kolmar Date: Wed, 22 Nov 2023 09:24:56 +0100 Subject: [PATCH 1/2] Added MpsExecute custom task --- CHANGELOG.md | 6 + README.md | 40 +++++++ api/mps-gradle-plugin.api | 15 +++ build.gradle.kts | 6 +- gradle.lockfile | 2 +- .../de/itemis/mps/gradle/ErrorMessages.kt | 4 + .../de/itemis/mps/gradle/common.gradle.kts | 5 + .../de/itemis/mps/gradle/tasks/MpsCheck.kt | 8 +- .../de/itemis/mps/gradle/tasks/MpsExecute.kt | 110 ++++++++++++++++++ 9 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a54b74..7c4c4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.21.0 + +### Added + +- `MpsExecute` custom task to execute specified method from a generated class. + ## 1.20.0 ### Added diff --git a/README.md b/README.md index 5e5d2ea..70184b4 100644 --- a/README.md +++ b/README.md @@ -464,6 +464,46 @@ Parameters: Compatibility note: `MpsCheck` task currently extends `JavaExec` but this may change in the future. Do not rely on this. +## `MpsExecute` Task Type + +A custom task to execute a specified method in a generated class. + +### Usage + +```groovy +import de.itemis.mps.gradle.tasks.MpsExecute + +plugins { + // Required in order to use the MpsExecute task + id("de.itemis.mps.gradle.common") +} + +tasks.register('executeMyTask', MpsExecute) { + mpsHome = file("...") // MPS home directory + + module = "my.module" + className = "my.module.GeneratedClass" + method = "myMethod" + methodArguments = ["arg1", "arg2"] +} +``` + +Parameters: + +* `projectLocation` - the location of the MPS project. Default is the Gradle project directory. +* `additionalExecuteBackendClasspath` - any extra libraries that should be on the classpath of the execute backend. +* `macros` - variables/macros that are necessary to open the project. +* `mpsHome` - the home directory of the MPS distribution (or RCP) to use for testing. +* `mpsVersion` - the MPS version, such as "2021.3". Default is autodetection by reading `$mpsHome/build.properties`. +* `pluginRoots` - directories containing additional plugins to load. +* `module` - the module that contains the generated class. +* `className` - fully qualified name of the generated class, that contains the method to execute. +* `method` - name of the method. The method should be public and static. Supported signatures are `(Project)` (from + `jetbrains.mps.project` model) or `(Project, String[])`. +* `methodArguments` - list of arguments to pass to the method. Default is an empty list. If arguments are provided the + method signature must be `(Project, String[])`. + + ## Run migrations Run all pending migrations in the project. diff --git a/api/mps-gradle-plugin.api b/api/mps-gradle-plugin.api index d54029c..5423944 100644 --- a/api/mps-gradle-plugin.api +++ b/api/mps-gradle-plugin.api @@ -323,3 +323,18 @@ public abstract class de/itemis/mps/gradle/tasks/MpsCheck : org/gradle/api/tasks public final fun getWarningAsError ()Lorg/gradle/api/provider/Property; } +public abstract class de/itemis/mps/gradle/tasks/MpsExecute : org/gradle/api/tasks/JavaExec { + public fun ()V + public fun exec ()V + public final fun getAdditionalExecuteBackendClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getClassName ()Lorg/gradle/api/provider/Property; + public abstract fun getMacros ()Lorg/gradle/api/provider/MapProperty; + public abstract fun getMethod ()Lorg/gradle/api/provider/Property; + public abstract fun getMethodArguments ()Lorg/gradle/api/provider/ListProperty; + public abstract fun getModule ()Lorg/gradle/api/provider/Property; + public abstract fun getMpsHome ()Lorg/gradle/api/file/DirectoryProperty; + public abstract fun getMpsVersion ()Lorg/gradle/api/provider/Property; + public abstract fun getPluginRoots ()Lorg/gradle/api/provider/SetProperty; + public abstract fun getProjectLocation ()Lorg/gradle/api/file/DirectoryProperty; +} + diff --git a/build.gradle.kts b/build.gradle.kts index f5b6fcf..0396b88 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ plugins { id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.2" } -val baseVersion = "1.20.0" +val baseVersion = "1.21.0" group = "de.itemis.mps" @@ -79,6 +79,10 @@ gradlePlugin { id = "modelcheck" implementationClass = "de.itemis.mps.gradle.modelcheck.ModelcheckMpsProjectPlugin" } + register("execute") { + id = "execute" + implementationClass = "de.itemis.mps.gradle.execute.ExecuteMpsProjectPlugin" + } register("migrations-executor") { id = "run-migrations" implementationClass = "de.itemis.mps.gradle.runmigrations.RunMigrationsMpsProjectPlugin" diff --git a/gradle.lockfile b/gradle.lockfile index fc2b52e..cd4a23c 100644 --- a/gradle.lockfile +++ b/gradle.lockfile @@ -1,7 +1,7 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. -de.itemis.mps.build-backends:launcher:2.1.0.58.0fb483b=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +de.itemis.mps.build-backends:launcher:2.1.0.62.f5dd7a0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath junit:junit:4.13.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath net.swiftzer.semver:semver:1.1.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath diff --git a/src/main/kotlin/de/itemis/mps/gradle/ErrorMessages.kt b/src/main/kotlin/de/itemis/mps/gradle/ErrorMessages.kt index d43da02..9cf025f 100644 --- a/src/main/kotlin/de/itemis/mps/gradle/ErrorMessages.kt +++ b/src/main/kotlin/de/itemis/mps/gradle/ErrorMessages.kt @@ -1,7 +1,11 @@ package de.itemis.mps.gradle +import java.io.File + internal object ErrorMessages { const val MUST_SET_CONFIG_OR_VERSION = "Either mpsConfig or mpsVersion needs to specified!" const val MUST_SET_VERSION_AND_LOCATION = "Setting an MPS version but no MPS location is not supported!" const val MPS_VERSION_NOT_SUPPORTED = "This version of mps-gradle-plugin only supports MPS 2020.1 and above. Please use version 1.4 with an older version of MPS." + + fun noMpsProjectIn(dir: File): String = "Directory does not contain an MPS project: $dir" } diff --git a/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts b/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts index a43f291..6a71799 100644 --- a/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts +++ b/src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts @@ -6,7 +6,12 @@ package de.itemis.mps.gradle */ val modelcheckBackend by configurations.creating +val executeBackend by configurations.creating modelcheckBackend.defaultDependencies { add(dependencies.create("de.itemis.mps.build-backends:modelcheck:${MPS_BUILD_BACKENDS_VERSION}")) } + +executeBackend.defaultDependencies { + add(dependencies.create("de.itemis.mps.build-backends:execute:${MPS_BUILD_BACKENDS_VERSION}")) +} diff --git a/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt index 35b8bc6..aaf60c0 100644 --- a/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt +++ b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt @@ -1,5 +1,6 @@ package de.itemis.mps.gradle.tasks +import de.itemis.mps.gradle.ErrorMessages import de.itemis.mps.gradle.launcher.MpsBackendBuilder import de.itemis.mps.gradle.launcher.MpsVersionDetection import org.gradle.api.GradleException @@ -13,7 +14,6 @@ import org.gradle.api.tasks.* import org.gradle.kotlin.dsl.* import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.process.CommandLineArgumentProvider -import java.io.File @CacheableTask abstract class MpsCheck : JavaExec(), VerificationTask { @@ -150,7 +150,7 @@ abstract class MpsCheck : JavaExec(), VerificationTask { override fun exec() { val projectLocationAsFile = projectLocation.get().asFile if (!projectLocationAsFile.resolve(".mps").isDirectory) { - throw GradleException(MpsCheckErrors.noMpsProjectIn(projectLocationAsFile)) + throw GradleException(ErrorMessages.noMpsProjectIn(projectLocationAsFile)) } super.exec() @@ -169,7 +169,3 @@ abstract class MpsCheck : JavaExec(), VerificationTask { include("plugins/git4idea/**/*.jar") } } - -internal object MpsCheckErrors { - fun noMpsProjectIn(dir: File): String = "Directory does not contain an MPS project: " + dir -} diff --git a/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt new file mode 100644 index 0000000..5eb7905 --- /dev/null +++ b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt @@ -0,0 +1,110 @@ +package de.itemis.mps.gradle.tasks + +import de.itemis.mps.gradle.ErrorMessages +import de.itemis.mps.gradle.launcher.MpsBackendBuilder +import de.itemis.mps.gradle.launcher.MpsVersionDetection +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.logging.LogLevel +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.newInstance +import org.gradle.work.DisableCachingByDefault + + +@DisableCachingByDefault(because = "calls arbitrary user code") +abstract class MpsExecute : JavaExec() { + + @get:Internal + abstract val mpsHome: DirectoryProperty + + @get:Internal + @get:Optional + abstract val mpsVersion: Property + + @get:Internal + @get:Optional + abstract val projectLocation: DirectoryProperty + + @get:Classpath + abstract val pluginRoots: SetProperty + + @get:Internal + abstract val macros: MapProperty + + @get:Internal + abstract val module: Property + + @get:Internal + abstract val className: Property + + @get:Internal + abstract val method: Property + + @get:Internal + abstract val methodArguments: ListProperty + + @get:Internal + val additionalExecuteBackendClasspath: ConfigurableFileCollection = + objectFactory.fileCollection().from(initialExecuteBackendClasspath()) + + init { + mpsVersion.convention(MpsVersionDetection.fromMpsHome(project.layout, providerFactory, mpsHome.asFile)) + projectLocation.convention(project.layout.projectDirectory) + + objectFactory.newInstance(MpsBackendBuilder::class) + .withMpsHomeDirectory(mpsHome) + .withMpsVersion(mpsVersion) + .configure(this) + + argumentProviders.add { + mutableListOf().apply { + add("--project=${projectLocation.get().asFile}") + + pluginRoots.get().forEach { + findPluginsRecursively(it.asFile).forEach { + add("--plugin=${it.id}::${it.path}") + } + } + macros.get().forEach { add("--macro=${it.key}::${it.value}") } + + add("--module=${module.get()}") + add("--class=${className.get()}") + add("--method=${method.get()}") + methodArguments.get().forEach { add("--arg=$it") } + + val effectiveLogLevel = logging.level ?: project.logging.level ?: project.gradle.startParameter.logLevel + if (effectiveLogLevel <= LogLevel.INFO) { + add("--log-level=info") + } + } + } + + description = "Execute specified method from a generated class to modify the MPS project" + group = "execute" + + classpath(project.configurations.named("executeBackend")) + classpath(additionalExecuteBackendClasspath) + + mainClass.set("de.itemis.mps.gradle.execute.MainKt") + } + + @TaskAction + override fun exec() { + val projectLocationAsFile = projectLocation.get().asFile + if (!projectLocationAsFile.resolve(".mps").isDirectory) { + throw GradleException(ErrorMessages.noMpsProjectIn(projectLocationAsFile)) + } + + super.exec() + } + + private fun initialExecuteBackendClasspath() = mpsHome.asFileTree.matching { + include("lib/**/*.jar") + } +} From 026105d275dd9ec54a6f2944029956fac8e07525 Mon Sep 17 00:00:00 2001 From: Konstantin Kolmar Date: Wed, 22 Nov 2023 22:03:07 +0100 Subject: [PATCH 2/2] Tests for MpsExecute --- .../de/itemis/mps/gradle/tasks/MpsExecute.kt | 2 - .../de/itemis/mps/gradle/MpsCheckTaskTest.kt | 6 +- .../itemis/mps/gradle/MpsExecuteTaskTest.kt | 118 ++++++++++++++++++ .../solutions/NewSolution/NewSolution.msd | 4 + .../models/NewSolution.myModel.mps | 86 +++++++++++++ 5 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 src/test/kotlin/test/de/itemis/mps/gradle/MpsExecuteTaskTest.kt diff --git a/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt index 5eb7905..a716c8f 100644 --- a/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt +++ b/src/main/kotlin/de/itemis/mps/gradle/tasks/MpsExecute.kt @@ -24,11 +24,9 @@ abstract class MpsExecute : JavaExec() { abstract val mpsHome: DirectoryProperty @get:Internal - @get:Optional abstract val mpsVersion: Property @get:Internal - @get:Optional abstract val projectLocation: DirectoryProperty @get:Classpath diff --git a/src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt b/src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt index cbbb64c..0d9f630 100644 --- a/src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt +++ b/src/test/kotlin/test/de/itemis/mps/gradle/MpsCheckTaskTest.kt @@ -1,8 +1,6 @@ package test.de.itemis.mps.gradle -import de.itemis.mps.gradle.tasks.MpsCheckErrors -import org.gradle.api.GradleException -import org.gradle.api.invocation.Gradle +import de.itemis.mps.gradle.ErrorMessages import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome import org.hamcrest.CoreMatchers.containsString @@ -67,7 +65,7 @@ class MpsCheckTaskTest { val result = gradleRunner().withArguments("checkProject").buildAndFail() Assert.assertEquals(TaskOutcome.FAILED, result.task(":checkProject")?.outcome) - assertThat(result.output, containsString(MpsCheckErrors.noMpsProjectIn(testProjectDir.root.canonicalFile))) + assertThat(result.output, containsString(ErrorMessages.noMpsProjectIn(testProjectDir.root.canonicalFile))) } @Test diff --git a/src/test/kotlin/test/de/itemis/mps/gradle/MpsExecuteTaskTest.kt b/src/test/kotlin/test/de/itemis/mps/gradle/MpsExecuteTaskTest.kt new file mode 100644 index 0000000..f2b0862 --- /dev/null +++ b/src/test/kotlin/test/de/itemis/mps/gradle/MpsExecuteTaskTest.kt @@ -0,0 +1,118 @@ +package test.de.itemis.mps.gradle + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class MpsExecuteTaskTest { + @Rule + @JvmField + val testProjectDir: TemporaryFolder = TemporaryFolder() + + private lateinit var buildFile: File + private lateinit var mpsTestProjectPath: File + + @Before + fun setUp() { + buildFile = testProjectDir.newFile("build.gradle.kts") + + val settingsFile = testProjectDir.newFile("settings.gradle.kts") + settingsFile.writeText(settingsScriptBoilerplate()) + + mpsTestProjectPath = testProjectDir.newFolder("mps-prj") + extractTestProject("test-project", mpsTestProjectPath) + } + + private fun settingsScriptBoilerplate() = """ + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version ("0.7.0") + } + """.trimIndent() + + private fun buildScriptBoilerplate(mpsVersion: String) = """ + import de.itemis.mps.gradle.tasks.MpsExecute + + plugins { + id("de.itemis.mps.gradle.common") + id("generate-models") + } + + repositories { + mavenCentral() + maven("https://artifacts.itemis.cloud/repository/maven-mps") + } + + val mps = configurations.create("mps") + + dependencies { + mps("com.jetbrains:mps:$mpsVersion") + } + + val resolveMps by tasks.registering(Sync::class) { + from({ zipTree(mps.singleFile) }) + into(layout.buildDirectory.dir("mps")) + } + + generate { + projectLocation = file("${mpsTestProjectPath.canonicalPath}") + mpsConfig = mps + } + + val generate by tasks.existing { + dependsOn(resolveMps) + doFirst { + println(layout.buildDirectory.dir("mps").get().asFile.listFiles()?.toList()) + } + } + + val execute by tasks.registering(MpsExecute::class) { + dependsOn(generate) + mpsHome.set(layout.buildDirectory.dir("mps")) + projectLocation.set(file("${mpsTestProjectPath.canonicalPath}")) + doFirst { + println(resolveMps.map { it.destinationDir }.get()) + } + } + """.trimIndent() + "\n" + + @Test + fun `execute with Project`() { + buildFile.writeText(buildScriptBoilerplate("2021.3.4") + """ + execute { + module.set("NewSolution") + className.set("NewSolution.myModel.MyClass") + method.set("onlyProject") + } + """.trimIndent()) + + val result = gradleRunner().withArguments("execute").build() + + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":execute")?.outcome) + } + + @Test + fun `execute with Project and args`() { + buildFile.writeText(buildScriptBoilerplate("2021.3.4") + """ + execute { + module.set("NewSolution") + className.set("NewSolution.myModel.MyClass") + method.set("projectAndArgs") + + methodArguments.set(listOf("arg1", "arg2")) + } + """.trimIndent()) + + val result = gradleRunner().withArguments("execute").build() + + Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":execute")?.outcome) + } + + private fun gradleRunner(): GradleRunner = GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withPluginClasspath() +} diff --git a/src/test/resources/test-project/solutions/NewSolution/NewSolution.msd b/src/test/resources/test-project/solutions/NewSolution/NewSolution.msd index 0b2f2da..188f59c 100644 --- a/src/test/resources/test-project/solutions/NewSolution/NewSolution.msd +++ b/src/test/resources/test-project/solutions/NewSolution/NewSolution.msd @@ -13,6 +13,7 @@ 6354ebe7-c22a-4a0f-ac54-50b52ab9b065(JDK) + 6ed54515-acc8-4d1e-a16c-9fd6cfe951ea(MPS.Core) @@ -29,7 +30,10 @@ + + + diff --git a/src/test/resources/test-project/solutions/NewSolution/models/NewSolution.myModel.mps b/src/test/resources/test-project/solutions/NewSolution/models/NewSolution.myModel.mps index 8a525c3..6d4064b 100644 --- a/src/test/resources/test-project/solutions/NewSolution/models/NewSolution.myModel.mps +++ b/src/test/resources/test-project/solutions/NewSolution/models/NewSolution.myModel.mps @@ -5,12 +5,17 @@ + + + + + @@ -53,16 +58,35 @@ + + + + + + + + + + + + + + + + + + + @@ -93,6 +117,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +