A Gradle plugin to package and publish MPS plugins. Use it if you have developed a language in MPS and you want to make it easy for other developers to use your language in their projects.
Publishing MPS-based IDEs (rich client platforms) is out of scope for this plugin.
This plugin also does not cover publishing to the JetBrains Plugins Repository.
This plugin has been tested with the following combinations of Gradle and MPS:
- Gradle 7.1 or above is required since version 1.2.0.
- Gradle 7.2 and MPS 2020.3.5 (version 1.1.0),
- Gradle 5.6.2 and MPS 2019.2.4 (version 1.0.0),
- Gradle 5.6.2 and MPS 2019.1.5 (version 0.0.2).
To simplify the configuration the plugin makes certain assumptions and therefore your project must follow certain conventions.
The following assumptions will hold if you let the MPS wizard generate your build solution and script:
- The MPS project directory is assumed to coincide with the Gradle project directory.
- The MPS project must contain a build model that is either named
build.mpsor matches*.build.mps. The build model must use default persistence and not file-per-root persistence. - The build model must generate a
build.xmlfile in the project's root directory. - The variable that specifies the location of MPS in the build script is called
mps_home.
The following conventions are different from the defaults and require manual adjustment of the generated build script:
- The default layout should also include the build solution.
- Instead of creating a .zip of all modules or plugins the default layout should just collect them in the top-level
folder:
or
default layout: module com.mbeddr.mpsutil.common module com.mbeddr.mpsutil.common.builddefault layout: plugin com.mbeddr.mpsutil.common [auto packaging] <empty> - Any dependencies will be put under
build/dependenciesfolder and can be referenced from the project's base directory (without using any path variables).
Some conventions can be customized via the mpsDefaults extension:
mpsDefaults {
// Custom MPS home directory
mpsHome.set(File("/my/mps/home"))
// Custom location and name of the build script
buildScript.set(file("build/build.xml"))
// Custom dependencies directory
dependenciesDirectory.set("build/project-libraries")
}A sample project using the plugin can be found here: https://github.com/specificlanguages/mps-gradle-plugin-sample.
All code snippets below use Kotlin syntax for Gradle.
-
Apply the plugin:
plugins { id("com.specificlanguages.mps") version "1.5.0" } -
Add the itemis mbeddr repository and the Maven Central repository to the project:
repositories { maven(url = "https://projects.itemis.de/nexus/content/repositories/mbeddr") mavenCentral() }The itemis mbeddr repository is used to download MPS as well as a small runner program to launch MPS from the command line. (The launcher is part of mps-build-backends.) Maven Central repository contains the Kotlin libraries that the launcher depends on.
-
Use the
mpsconfiguration to specify the MPS version to use:dependencies { "mps"("com.jetbrains:mps:2021.1.4") } -
Include the dependencies necessary for generation into the
generationconfiguration:dependencies { "generation"("de.itemis.mps:extensions:2021.1.+") } -
(Since 1.2.0) Specify JAR dependencies that are used as stubs and where they should be placed:
stubs { register("stubs") { destinationDir("solutions/my.stubs/lib") dependency("org.apache.commons:commons-lang3:3.12.0") ... } ... } -
For publishing apply the
maven-publishplugin and usefrom(components["mps"]):plugins { ... `maven-publish` } ... publishing { publications { register<MavenPublication>("mpsPlugin") { from(components["mps"]) // Put resolved versions of dependencies into POM files versionMapping { usage("java-runtime") { fromResolutionOf("generation") } } } } }
The plugin applies the Gradle Base plugin which creates a
set of lifecycle tasks such as clean, assemble, check, or build.
The plugin creates the following tasks:
setup: unpacks all dependencies of thegenerationconfiguration intobuild/dependencies. Since 1.2.0 it also triggers the individual tasks to unpack stubs, see below. Executing thesetuptask is required before the project can be opened in MPS.(Removed in 1.4.0.)resolveMpsForGeneration: downloads the MPS artifact specified by thempsconfiguration and unpacks it intobuild/mps.resolve<StubName>: a Sync task created for each stub block (<StubName>is the name of the stub entry, capitalized). The task resolves the dependencies configured in the stub block and copies them into the specified destination directory of the stub, deleting any other files present in that directory. When copying the dependencies their version numbers are stripped. This makes it possible to upgrade dependency versions in the Gradle build script without also reconfiguring the MPS stub solutions.generateBuildscript: generates the build script using MPS. The build model location is detected automatically. Since 1.2.2 the task specifies all module files (*.msd,*.mpl,*.devkit) as its inputs since build scripts check their contents against the modules.assembleMps: runsgenerateandassembletargets of the generated Ant script. Theassemblelifecycle task is set to depend onassembleMps. Since 1.1.1 the version of the project is passed to Ant via-Dversion=${project. version}. Since 1.2.2 the task specifies all*.mpsfiles in all directories as its inputs.checkMps: runs thechecktarget of the generated Ant script. Thechecklifecycle task is set to depend oncheckMps.package: packages the modules built byassembleMpsin a ZIP. The package is added to thedefaultconfiguration (created by the Gradle Base plugin).
The plugin creates a software component named mps to represent the published code and adds the default configuration
to it.
The plugin modifies the clean task to delete MPS-generated directories: source_gen, source_gen.caches,
classes_gen, tests_gen, and tests_gen.caches. This is in addition to the default operation of clean task which
deletes the project's build directory (build).
Before version 1.4.0 the plugin declared a task, resolveMpsForGeneration, to download and unzip the MPS distribution
under build/mps. In 1.4.0 and above this is handled via Gradle artifact transforms. This should enable sharing the
MPS distributions between projects that use the same MPS version (for example, among subprojects of a large project).
The task is left empty in 1.4.0 for backwards compatibility purposes.
flowchart RL
setup --> resolveStub["resolve{STUB1,...,STUBn}"]
setup --> resolveGenerationDependencies
assemble --> assembleMps
package --> assembleMps
checkMps --> assembleMps
check --> checkMps
assembleMps --> generateBuildscript
generateBuildscript --> setup
The plugin creates the following configurations:
mps- the distribution of MPS to use.generation- MPS libraries that the project depends on (such as mbeddr platform or MPS-extensions).executeGenerators- can be used to override the version of theexecute-generatorsbackend. If left unconfigured, a reasonable default will be used.ant- the Ant classpath. If left unconfigured, contains anant-junitdependency. Can be customized if you need extra libraries on the Ant classpath.
All dependencies added to generation configuration will have their artifact type set to zip. This is important for
compatibility with Maven.
Further configurations will be created by the stubs block.
MPS code can make use of Java libraries after loading their JAR files as Java stubs. As a best practice these JARs
should be placed into a lib folder underneath the solution that will load them.
To avoid bloating your source code repository with binary files, this plugin can download these libraries during the
build and place them into a configured directory. The aforementioned lib folder can then be added to .gitignore
file (or equivalent).
Configure the stubs as follows:
stubs {
register("myStubs") {
destinationDir("solutions/my.stubs/lib")
dependency("org.apache.commons:commons-lang3:3.12.0")
...
}
...
}This declaration has several effects:
- A configuration is created with the same name as the stub entry (
myStubs) in the example above. The dependencies are added to that configuration. - A task is created to download the dependencies and put them under the destination directory (
solutions/my.stubs/libin the example above). The name of the configuration will beresolve<StubName>where<StubName>is the capitalized name of the stub configuration. In the example above the name of the task will beresolveMyStubs.
Notes:
- The task is of type Sync. It will delete any extra files present in the destination directory.
- When copying the dependencies their version number will be stripped from the JAR file names.
commons-lang3-3.12.0.jarwill becomecommons-lang3.jar. Upgrading the dependency version in Gradle will not require updating the stub solution configuration in MPS.
(Since 1.2.2) Several tasks created by this plugin attempt to specify their input dependencies to make sure that incremental builds work properly, i.e. the project is not rebuilt on irrelevant changes and is rebuilt on relevant ones. The following tasks specify their inputs:
assembleMpsdepends on all*.mpsfiles in the project.generateBuildscriptdepends on the build model but also on all module files in the project (*.msd,*.mpl,*.devkit) since the generator checks the build model against the modules during generation.
Getting the dependencies 100 % correct is difficult so it is possible that Gradle would build the project even if it should not. However, if you find that Gradle is NOT rebuilding your project when it should, please report this.
You can do a clean build of the project by running ./gradlew clean build.
The plugin exposes a task type to run Ant scripts, named com.specificlanguages.RunAntScript. This task can be used
to run arbitrary targets of arbitrary Ant scripts. It is pre-configured with conventions when the plugin is applied,
to minimize the amount of necessary configuration.
For example, you can create a Gradle task to invoke myBuild.xml and call target myTarget:
val myTarget by tasks.registering(RunAntScript::class) {
buildScript.set(file("myBuild.xml")) // default: mpsDefaults.buildScript
classpath.set(configurations.named("myAnt")) // default: 'ant' configuration
antProperties.put("mps_home" to "/my/mps/home") // default: mps_home = mpsDefaults.mpsHome
// version = project.version
targets.set(listOf("myTarget")) // no default
}