Skip to content

Commit

Permalink
1.23.0: Add MpsMigrate task
Browse files Browse the repository at this point in the history
  • Loading branch information
sergej-koscejev committed Feb 14, 2024
1 parent 1a5f477 commit f28e9fc
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 1 deletion.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.23.0

### Added

- `MpsMigrate` task for migrating a project (or projects), similar to the `runMigrations` extension.

## 1.22.2

### Fixed
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,41 @@ Parameters:

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

## `MpsMigrate` Task Type

Migrates the specified projects.

### Usage

```groovy
import de.itemis.mps.gradle.tasks.MpsMigrate
plugins {
// Required in order to use the task
id("de.itemis.mps.gradle.common")
}
tasks.register('migrate', MpsMigrate) {
mpsHome = mpsHomeDir
// MpsMigrate task can migrate multiple projects at once
projectDirectories.from(projectDir)
...
}
```

Parameters:

* `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.
* `haltOnPrecheckFailure` - fail if the migration pre-check (e.g. broken references) fails.
* `haltOnDependencyError` - fail if non-migrated dependencies are found.
* `projectDirectories` - project directories to migrate.
* `folderMacros` - path variables/macros that are necessary to open the project. Path macros are not considered part of
Gradle build cache key.
* `pluginRoots` - directories that will be searched (recursively) for additional plugins to load.

## Run migrations

Run all pending migrations in the project.
Expand Down
18 changes: 18 additions & 0 deletions api/mps-gradle-plugin.api
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,21 @@ public abstract class de/itemis/mps/gradle/tasks/MpsGenerate : org/gradle/api/ta
public final fun getVarMacros ()Lorg/gradle/api/provider/MapProperty;
}

public abstract class de/itemis/mps/gradle/tasks/MpsMigrate : org/gradle/api/DefaultTask {
public fun <init> (Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/provider/ProviderFactory;)V
public final fun execute ()V
protected final fun getAllProjectFiles ()Lorg/gradle/api/provider/Provider;
public final fun getAntJvmArgs ()Lorg/gradle/api/provider/ListProperty;
public final fun getFolderMacros ()Lorg/gradle/api/provider/MapProperty;
public final fun getHaltOnDependencyError ()Lorg/gradle/api/provider/Property;
public final fun getHaltOnPrecheckFailure ()Lorg/gradle/api/provider/Property;
public final fun getJavaExecutable ()Lorg/gradle/api/file/RegularFileProperty;
public final fun getJavaLauncher ()Lorg/gradle/api/provider/Property;
public final fun getJvmArgs ()Lorg/gradle/api/provider/ListProperty;
public final fun getMaxHeapSize ()Lorg/gradle/api/provider/Property;
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/file/ConfigurableFileCollection;
public final fun getProjectDirectories ()Lorg/gradle/api/file/ConfigurableFileCollection;
}

2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ plugins {
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.2"
}

val baseVersion = "1.22.2"
val baseVersion = "1.23.0"

group = "de.itemis.mps"

Expand Down
36 changes: 36 additions & 0 deletions src/main/kotlin/de/itemis/mps/gradle/tasks/Libraries.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package de.itemis.mps.gradle.tasks

import org.gradle.api.file.Directory
import java.io.File
import java.io.FileNotFoundException

internal fun readLibraries(projectDir: File, getMacroPath: (String) -> Directory?) = try {
projectDir.resolve(".mps/libraries.xml").useLines { lines ->
lines
.map { pathRegex.find(it) }
.filterNotNull()
.map { it.groups[1]!!.value }
.map { substituteMacros(it, projectDir, getMacroPath) }
.toList()
}
} catch (e : FileNotFoundException) {
listOf()
}

private val pathRegex = """<option name=["']path["'] value=["']([^"']+)["']""".toRegex()

private fun substituteMacros(input: String, projectDir: File, getMacro: (String) -> Directory?): String {
if (input.startsWith("\$PROJECT_DIR$"))
return projectDir.toString() + input.substring("\$PROJECT_DIR$".length)
else if (input.startsWith("\${")) {
val closingBrace = input.indexOf('}')
if (closingBrace < 0) return input

val macro = input.substring(2, closingBrace)
val folder = getMacro(macro) ?: return input

return folder.toString() + input.substring(closingBrace + 1)
}

return input
}
222 changes: 222 additions & 0 deletions src/main/kotlin/de/itemis/mps/gradle/tasks/MpsMigrate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package de.itemis.mps.gradle.tasks

import de.itemis.mps.gradle.launcher.MpsVersionDetection
import de.itemis.mps.gradle.runAnt
import groovy.xml.MarkupBuilder
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.LogLevel
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.*
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.kotlin.dsl.listProperty
import org.gradle.kotlin.dsl.mapProperty
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.withGroovyBuilder
import org.gradle.platform.Architecture
import java.io.File
import javax.inject.Inject

@UntrackedTask(because = "May migrate the project, too difficult to track")
abstract class MpsMigrate @Inject constructor(
objectFactory: ObjectFactory,
providerFactory: ProviderFactory
) : DefaultTask() {

@get:Internal
val mpsHome: DirectoryProperty = objectFactory.directoryProperty()

@get:Input
@get:Optional
val mpsVersion: Property<String> = objectFactory.property<String>()
.convention(MpsVersionDetection.fromMpsHome(project.layout, providerFactory, mpsHome.asFile))

/**
* (Since MPS 2021.1) Whether to halt if a pre-check has failed. Note that to ignore the check for migrated
* dependencies the [haltOnDependencyError] option must be set to `false` as well.
*/
@get:Input
@get:Optional
val haltOnPrecheckFailure: Property<Boolean> = objectFactory.property<Boolean>()

/**
* (Since MPS 2021.3.4) Whether to halt when a non-migrated dependency is discovered.
*/
@get:Input
@get:Optional
val haltOnDependencyError: Property<Boolean> = objectFactory.property<Boolean>()

@get:Internal("covered by allProjectFiles")
val projectDirectories: ConfigurableFileCollection = objectFactory.fileCollection()

@get:InputFiles
@get:SkipWhenEmpty
protected val allProjectFiles = providerFactory.provider { projectDirectories.flatMap { objectFactory.fileTree().from(it) } }

@get:Internal
val javaExecutable: RegularFileProperty = objectFactory.fileProperty()

@get:Nested
@get:Optional
val javaLauncher: Property<JavaLauncher> = objectFactory.property()

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

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

@get:Input
@get:Optional
val maxHeapSize: Property<String> = objectFactory.property()

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

@get:Classpath
val pluginRoots: ConfigurableFileCollection = objectFactory.fileCollection()

@TaskAction
fun execute() {
val buildFile = temporaryDir.resolve("build.xml")
writeBuildFile(buildFile)

val mpsAntClasspath = mpsHome.asFileTree.matching {
include("lib/ant/lib/*.jar")
include("lib/*.jar")
}

project.javaexec {
mainClass.set("org.apache.tools.ant.launch.Launcher")
workingDir = temporaryDir
classpath = mpsAntClasspath
val executableCandidate = javaExecutable.orElse(javaLauncher.map { it.executablePath }).orNull?.toString()
if (executableCandidate != null) {
executable = executableCandidate
}
jvmArgs(antJvmArgs.get())
}
}

private fun writeBuildFile(buildFile: File) {
buildFile.printWriter().use { writer ->
MarkupBuilder(writer).withGroovyBuilder {
"project" {
"path"("id" to "path.mps.ant.path") {
// The different MPS versions need different jars. Let's just keep it simple and include all jars.
"fileset"("dir" to "${mpsHome.get()}/lib", "includes" to "**/*.jar")
}
"taskdef"(
"resource" to "jetbrains/mps/build/ant/antlib.xml",
"classpathref" to "path.mps.ant.path"
)

val argsToMigrate = mutableListOf<Pair<String, Any>>().run {
add("mpsHome" to mpsHome.get())

if (haltOnPrecheckFailure.isPresent) { add("haltOnPrecheckFailure" to haltOnPrecheckFailure.get()) }
if (haltOnDependencyError.isPresent) { add("haltOnDependencyError" to haltOnDependencyError.get()) }

if (mpsVersion.get() >= "2022.3") { add("jnaLibraryPath" to "lib/jna/${computeJnaArch()}") }

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

toTypedArray()
}

val folderMacrosValue = folderMacros.get()
val allLibraries = projectDirectories
.flatMap { readLibraries(it, folderMacrosValue::get) }
.toSortedSet()

"migrate"(*argsToMigrate) {
projectDirectories.forEach {
"project"("path" to it)
}

"repository" {
allLibraries.forEach { "modules"("dir" to it) }
}

"macro"("name" to "mps_home", "path" to mpsHome.get())

folderMacrosValue.forEach {
"macro"("name" to it.key, "path" to it.value)
}

"jvmargs" {
"arg"("value" to "-Didea.log.config.file=log.xml")
"arg"("value" to "-Didea.config.path=${temporaryDir}/config")
"arg"("value" to "-Didea.system.path=${temporaryDir}/system")
"arg"("value" to "-ea")

if (maxHeapSize.isPresent) {
"arg"("value" to "-Xmx${maxHeapSize.get()}")
}

"arg"("value" to "--add-opens=java.base/java.io=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.lang=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.net=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.nio=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.nio.charset=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.text=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.time=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.util=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/jdk.internal.vm=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/sun.security.ssl=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.base/sun.security.util=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/java.awt=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/java.awt.dnd.peer=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/java.awt.event=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/java.awt.image=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/java.awt.peer=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/javax.swing=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/javax.swing.plaf.basic=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/javax.swing.text.html=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/sun.awt.datatransfer=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/sun.awt.image=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/sun.awt=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/sun.font=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/sun.java2d=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/sun.swing=ALL-UNNAMED")
"arg"("value" to "--add-opens=jdk.attach/sun.tools.attach=ALL-UNNAMED")
"arg"("value" to "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED")
"arg"("value" to "--add-opens=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED")
"arg"("value" to "--add-opens=jdk.jdi/com.sun.tools.jdi=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/com.apple.laf=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/com.apple.eawt=ALL-UNNAMED")
"arg"("value" to "--add-opens=java.desktop/com.apple.eawt.event=ALL-UNNAMED")

jvmArgs.get().forEach {
"arg"("value" to it)
}
}

pluginRoots.flatMap { findPluginsRecursively(it) }.forEach {
"plugin"("path" to it.path, "id" to it.id)
}
}
}
}
}
}
}

private fun computeJnaArch(): String = when (System.getProperty("os.arch")) {
"aarch64" -> "aarch64"
else -> "amd64"
}
38 changes: 38 additions & 0 deletions src/test/kotlin/test/de/itemis/mps/gradle/RunMigrationsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.testkit.runner.TaskOutcome
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand Down Expand Up @@ -172,6 +173,43 @@ class RunMigrationsTest {
assertThat(result.output, containsString("Specified MPS location does not exist or is not a directory:"))
}

@Test
fun `MpsMigrate task works`() {
buildFile.writeText(
"""
import de.itemis.mps.gradle.tasks.MpsMigrate
plugins {
id("de.itemis.mps.gradle.common")
}
repositories {
mavenCentral()
maven("https://artifacts.itemis.cloud/repository/maven-mps")
}
val mps = configurations.create("mps")
dependencies {
mps("com.jetbrains:mps:2021.3.2")
}
val resolveMps by tasks.registering(Sync::class) {
from(Callable { zipTree(mps.singleFile) })
into(layout.buildDirectory.dir("mps"))
}
val migrate by tasks.registering(MpsMigrate::class) {
projectDirectories.from("$mpsTestPrjLocation")
mpsHome.set(layout.dir(resolveMps.map { it.destinationDir }))
}
""".trimIndent()
)

val result = gradleRunner().withArguments("migrate").build()

assertEquals(TaskOutcome.SUCCESS, result.task(":migrate")?.outcome)
}

private fun gradleRunner(): GradleRunner = GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withPluginClasspath()
Expand Down

0 comments on commit f28e9fc

Please sign in to comment.