Skip to content

Commit

Permalink
1.19: Add MpsCheck task
Browse files Browse the repository at this point in the history
  • Loading branch information
sergej-koscejev committed Sep 25, 2023
1 parent f16f039 commit 6478154
Show file tree
Hide file tree
Showing 11 changed files with 606 additions and 21 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
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.19

### Added

- `MpsCheck` task for running the model checker. In contrast to the `modelcheck` plugin there can be multiple instances
of the task, and the task is written with the current Gradle best practices (lazy properties, caching).

## 1.18

### Changed
Expand Down
99 changes: 80 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
Miscellaneous tasks that were found useful when building MPS-based
projects with Gradle.

**Table of Contents**

<!-- TOC -->
* [mps-gradle-plugin](#mps-gradle-plugin)
* [Configuring the plugin repository](#configuring-the-plugin-repository)
* [Custom tasks](#custom-tasks)
* [RunAntScript](#runantscript)
* [Usage](#usage-)
* [Providing Global Defaults For Class Path And Arguments](#providing-global-defaults-for-class-path-and-arguments)
* [Providing Global Defaults For The Java Executable](#providing-global-defaults-for-the-java-executable)
* [Incremental Builds](#incremental-builds)
* [CreateDmg](#createdmg)
* [Usage](#usage)
* [Operation](#operation)
* [BundleMacosJdk](#bundlemacosjdk)
* [Usage](#usage-1)
* [Operation](#operation-1)
* [GenerateLibrariesXml](#generatelibrariesxml)
* [Usage](#usage-2)
* [Operation](#operation-2)
* [`generate`](#generate)
* [Usage](#usage-3)
* [`checkModels`](#checkmodels)
* [Usage](#usage-4)
* [Additional Plugins](#additional-plugins)
* [Additional Plugins](#additional-plugins-)
* [Run migrations](#run-migrations)
* [Usage](#usage-5)
* [Download JetBrains Runtime](#download-jetbrains-runtime)
* [Usage](#usage-6)
* [Parameters](#parameters)
* [Custom MPS Distribution](#custom-mps-distribution)
* [MPS vs IDEA environment](#mps-vs-idea-environment)
<!-- TOC -->

# Configuring the plugin repository

This plugin is not published to the Gradle plugin portal but to a public repository of itemis. To configure this
Expand Down Expand Up @@ -211,7 +246,7 @@ Each property represents an entry in `destination` (a project library),
where the property name is the library name and the property value is
the path to the library.

## Generate
## `generate`

Generate a specific or all models in a project without the need for a MPS model.

Expand Down Expand Up @@ -279,12 +314,12 @@ Parameters:
* `maxHeap` (since 1.15) - maximum heap size setting for the JVM that executes the generator. The value is a string
understood by the JVM command line argument `-Xmx` e.g. `3G` or `512M`.

## Model Check
## `checkModels`

Run the model check on a subset or all models in a project directly from gradle.

This functionality currently runs all model checks (typesystem, structure, constrains, etc.) from gralde. By default if
any of checks fails the complete build is failed. All messages (Info, Warning or Error) are reported through log4j to
This functionality currently runs all model checks (typesystem, structure, constrains, etc.) from Gradle. By default, if
any of checks fails, the complete build is failed. All messages (Info, Warning or Error) are reported through log4j to
the command line.

### Usage
Expand Down Expand Up @@ -358,7 +393,7 @@ Parameters:

### Additional Plugins

By default only the minimum required set of plugins are loaded. This includes base language and some utilities like the
By default, only the minimum required set of plugins is loaded. This includes base language and some utilities like the
HTTP server from MPS. If your project requires additional plugins to be loaded this is done by setting plugin location
to the place where your jar files are placed and adding your plugin id and folder name to the `plugins` list:

Expand All @@ -378,27 +413,53 @@ modelcheck {
Dependencies of the specified plugins are automatically loaded from the `pluginLocation` and the plugins directory of
MPS. If they are not found the build will fail.

### Additional Plugins
## `MpsCheck` Task Type

By default only the minimum required set of plugins are loaded. This includes base language and some utilities like the
HTTP server from MPS. If your project requires additional plugins to be loaded this is done by setting plugin location
to the place where your jar files are placed and adding your plugin id and folder name to the `plugins` list:
This task improves over the `modelcheck` plugin described above and fixes some of its deficiencies.

```
apply plugin: 'modelcheck'
...
The `modelcheck` extension provided by the eponymous plugin can only be configured once per Gradle project. Checking
multiple subprojects is not possible without resorting to tricks. In addition, the extension only has limited support
for lazy configuration and does not support Gradle build cache.

modelcheck {
pluginLocation.set(new File("path/to/my/plugins"))
plugins.set([new Plugin("com.mbeddr.core", "mbeddr.core")])
projectLocation.set(new File("./mps-prj"))
mpsConfig.set(configurations.mps)
The `MpsCheck` task works similarly to the `checkmodels` task of `modelcheck` plugin but allows defining multiple
instances of itself, supports lazy configuration and caching.

### Usage

```groovy
import de.itemis.mps.gradle.tasks.MpsCheck
plugins {
// Required in order to use the MpsCheck task
id("de.itemis.mps.gradle.common")
}
tasks.register('checkProject', MpsCheck) {
mpsHome = file("...") // MPS home directory
projectLocation = projectDir
}
```

Dependencies of the specified plugins are automatically loaded from the `pluginlocation` and the plugins directory of
MPS. If they are not found the the build will fail.
Parameters:

* `projectLocation` - the location of the project to check. Default is the Gradle project directory.
* `models`, `modules`, `excludeModels`, `excludeModules` - regular expressions. Matching modules and models will be
included or excluded from checking.
* `additionalModelcheckBackendClasspath` - any extra libraries that should be on the classpath of the modelcheck
backend.
* `folderMacros` - path variables/macros that are necessary to open the project. Path macros are not considered part of
Gradle build cache key.
* `varMacros` - non-path variables/macros that are necessary to open the project. Variable macros *are* considered part
of Gradle build cache key.
* `junitFile` - the JUnit XML file to produce. Defaults to `$buildDir/TEST-${task.name}.xml`
* `junitFormat` - the format of the JUnit XML file. Defaults to `module-and-model`.
* `mpsHome` - the home directory of the MPS distribution (or RCP) to use for testing.
* `mpsVersion` - the MPS version, such as "2021.3". Autodetected by reading `$mpsHome/build.properties` by default.
* `pluginRoots` - directories containing additional plugins to load
* `warningAsError` - whether to treat warnings as errors.
* `ignoreFailures` (inherited from `VerificationTask`) - whether to fail the build if an error is found.

Compatibility note: `MpsCheck` task currently extends `JavaExec` but this may change in the future. Do not rely on this.

## Run migrations

Expand Down
20 changes: 20 additions & 0 deletions api/mps-gradle-plugin.api
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,23 @@ public final class de/itemis/mps/gradle/runmigrations/RunMigrationsMpsProjectPlu
public final fun getMIN_VERSION_FOR_HALT_ON_PRECHECK_FAILURE ()Lnet/swiftzer/semver/SemVer;
}

public abstract class de/itemis/mps/gradle/tasks/MpsCheck : org/gradle/api/tasks/JavaExec, org/gradle/api/tasks/VerificationTask {
public fun <init> ()V
public fun exec ()V
public final fun getAdditionalModelcheckBackendClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection;
public final fun getExcludeModels ()Lorg/gradle/api/provider/ListProperty;
public final fun getExcludeModules ()Lorg/gradle/api/provider/ListProperty;
public final fun getFolderMacros ()Lorg/gradle/api/provider/MapProperty;
public final fun getJunitFile ()Lorg/gradle/api/file/RegularFileProperty;
public final fun getJunitFormat ()Lorg/gradle/api/provider/Property;
public final fun getModels ()Lorg/gradle/api/provider/ListProperty;
public final fun getModules ()Lorg/gradle/api/provider/ListProperty;
public final fun getMpsHome ()Lorg/gradle/api/file/DirectoryProperty;
public final fun getMpsVersion ()Lorg/gradle/api/provider/Property;
public final fun getPluginRoots ()Lorg/gradle/api/provider/SetProperty;
public final fun getProjectLocation ()Lorg/gradle/api/file/DirectoryProperty;
protected final fun getSources ()Lorg/gradle/api/provider/Provider;
public final fun getVarMacros ()Lorg/gradle/api/provider/MapProperty;
public final fun getWarningAsError ()Lorg/gradle/api/provider/Property;
}

6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ plugins {
}

val versionMajor = 1
val versionMinor = 18
val versionMinor = 19

group = "de.itemis.mps"

Expand Down Expand Up @@ -59,9 +59,13 @@ dependencies {
api("de.itemis.mps.gradle:git-based-versioning")
implementation(kotlin("stdlib", version = kotlinVersion))
implementation("net.swiftzer.semver:semver:1.1.2")
implementation("de.itemis.mps.build-backends:launcher:1.+")
testImplementation("junit:junit:4.13.2")
}

tasks.test {
useJUnit()
}

gradlePlugin {
plugins {
Expand Down
1 change: 1 addition & 0 deletions gradle.lockfile
Original file line number Diff line number Diff line change
@@ -1,6 +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:1.0.0.55.b7ebac5=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
Expand Down
4 changes: 4 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ pluginManagement {
includeBuild("git-based-versioning")
}

plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version ("0.7.0")
}

rootProject.name = "mps-gradle-plugin"

includeBuild("git-based-versioning")
8 changes: 7 additions & 1 deletion src/main/kotlin/de/itemis/mps/gradle/common.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package de.itemis.mps.gradle

/**
* Dummy empty plugin that lets us use `plugins` block rather than `buildscript` to put the task classes
* A side effect of this plugin is that it lets us use `plugins` block rather than `buildscript` to put the task classes
* ([RunAntScript], [BuildLanguages], etc.) onto the classpath.
*/

val modelcheckBackend by configurations.creating

modelcheckBackend.defaultDependencies {
add(dependencies.create("de.itemis.mps.build-backends:modelcheck:${MPS_BUILD_BACKENDS_VERSION}"))
}
160 changes: 160 additions & 0 deletions src/main/kotlin/de/itemis/mps/gradle/tasks/MpsCheck.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package de.itemis.mps.gradle.tasks

import de.itemis.mps.gradle.launcher.MpsBackendLauncher
import org.gradle.api.GradleException
import org.gradle.api.file.*
import org.gradle.api.logging.LogLevel
import org.gradle.api.provider.*
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 {

// Having our own private launcher instance means we don't need to apply the launcher plugin. This works as long
// as the launcher remains stateless.
private val backendLauncher: MpsBackendLauncher = objectFactory.newInstance(MpsBackendLauncher::class)

@get:InputDirectory
@get:PathSensitive(PathSensitivity.NONE)
val mpsHome: DirectoryProperty = objectFactory.directoryProperty()

@get:Input
@get:Optional
val mpsVersion: Property<String> = objectFactory.property<String>()
.convention(backendLauncher.mpsVersionFromMpsHome(mpsHome.asFile))

@get:Internal("only modules and models matter, covered by #sources")
val projectLocation: DirectoryProperty =
objectFactory.directoryProperty().convention(project.layout.projectDirectory)

@get:Classpath
val pluginRoots: SetProperty<Directory> = objectFactory.setProperty()

@get:Internal("Folder macros are ignored for the purposes of up-to-date checks and caching")
val folderMacros: MapProperty<String, Directory> = objectFactory.mapProperty()

@get:Input
val varMacros: MapProperty<String, String> = objectFactory.mapProperty()

@get:Input
val models: ListProperty<String> = objectFactory.listProperty()

@get:Input
val modules: ListProperty<String> = objectFactory.listProperty()

@get:Input
val excludeModels: ListProperty<String> = objectFactory.listProperty()

@get:Input
val excludeModules: ListProperty<String> = objectFactory.listProperty()

@get:Input
val warningAsError: Property<Boolean> = objectFactory.property<Boolean>().convention(false)

@get:OutputFile
val junitFile: RegularFileProperty = objectFactory.fileProperty()
.convention(project.layout.buildDirectory.map { it.file("TEST-${this@MpsCheck.name}.xml") })

@get:Input
val junitFormat: Property<String> = objectFactory.property<String>().convention("module-and-model")

@get:Internal("covered by classpath")
val additionalModelcheckBackendClasspath: ConfigurableFileCollection =
objectFactory.fileCollection().from(initialModelcheckBackendClasspath())

@Suppress("unused")
@InputFiles
@SkipWhenEmpty
@PathSensitive(PathSensitivity.NONE)
protected val sources: Provider<FileCollection> = projectLocation.map {
it.asFileTree.matching {
exclude(project.layout.buildDirectory.get().asFile.relativeTo(projectLocation.get().asFile).path + "/**")
include("**/*.msd")
include("**/*.mpsr")
include("**/*.mps")
}
}

init {
backendLauncher.configureJavaForMpsVersion(this, mpsHome.map { it.asFile }, mpsVersion)
argumentProviders.add(CommandLineArgumentProvider {
val result = mutableListOf<String>()

result.add("--project=${projectLocation.get().asFile}")

pluginRoots.get().flatMap { findPluginsRecursively(it.asFile) }
.mapTo(result) { "--plugin=${it.id}::${it.path}" }
folderMacros.get().mapTo(result) { "--macro=${it.key}::${it.value.asFile}" }
varMacros.get().mapTo(result) { "--macro=${it.key}::${it.value}" }

// Only a limited subset of checkers is registered in MPS environment, IDEA environment is necessary for
// proper checking.
result.add("--environment=IDEA")

result.addAll(models.get().map { "--model=$it" })
result.addAll(modules.get().map { "--module=$it" })
result.addAll(excludeModels.get().map { "--exclude-model=$it" })
result.addAll(excludeModules.get().map { "--exclude-module=$it" })

if (warningAsError.get()) {
result.add("--warning-as-error")
}

if (ignoreFailures) {
result.add("--error-no-fail")
}

if (junitFile.isPresent) {
result.add("--result-file=${junitFile.get().asFile}")
}

if (junitFormat.isPresent) {
result.add("--result-format=${junitFormat.get()}")
}

val effectiveLogLevel = logging.level ?: project.logging.level ?: project.gradle.startParameter.logLevel
if (effectiveLogLevel <= LogLevel.INFO) {
result.add("--log-level=info")
}

result
})

group = LifecycleBasePlugin.VERIFICATION_GROUP

classpath(project.configurations.named("modelcheckBackend"))
classpath(additionalModelcheckBackendClasspath)

mainClass.set("de.itemis.mps.gradle.modelcheck.MainKt")
}

override fun exec() {
val projectLocationAsFile = projectLocation.get().asFile
if (!projectLocationAsFile.resolve(".mps").isDirectory) {
throw GradleException(MpsCheckErrors.noMpsProjectIn(projectLocationAsFile))
}

super.exec()
}

private fun initialModelcheckBackendClasspath() = mpsHome.asFileTree.matching {
include("lib/**/*.jar")

// add only minimal number of plugins jars that are required by the modelcheck code
// (to avoid conflicts with plugin classloader if custom configured plugins are loaded)
// mps-httpsupport: we need it to print the node url to the console.
// mps-modelchecker: contains used UnresolvedReferencesChecker
// git4idea: has to be on classpath as bundled plugin to be loaded (since 2019.3)
include("plugins/mps-modelchecker/**/*.jar")
include("plugins/mps-httpsupport/**/*.jar")
include("plugins/git4idea/**/*.jar")
}
}

internal object MpsCheckErrors {
fun noMpsProjectIn(dir: File): String = "Directory does not contain an MPS project: " + dir
}
Loading

0 comments on commit 6478154

Please sign in to comment.