Creating a Command Line Tool

Basic code:

public class CommandLineTool { 

  public static void main(string[] args) throws Exception {
    final string projectPath = YOUR_PATH /* (1) Get project path, for example, from args or system properties */;

    final EnvironmentConfig config = EnvironmentConfig
      .emptyConfig()
      .withDefaultPlugins()
      .withBootstrapLibraries();
    config.addPlugin("http-support", "jetbrains.mps.ide.httpsupport");
    final IdeaEnvironment env = new IdeaEnvironment(config);
    env.init();

    final Project project = env.openProject(new File(projectPath));
    Throwable thrown = null;
    try {
        doSomething(); // 2
    } catch (Exception e) {
      thrown = e;
    } finally {
      env.dispose();
    }
    if (thrown != null) {
      System.err.println("ERROR:");
      thrown.printStackTrace();
      System.exit(1);
    } else {
      // You need `System.exit` even in a successful case to stop threads that MPS plugins may be leaving behind.
      System.exit(0);
    }
  }
}

What can you do with the project (at point 2)? Here is how you invoke a static method foo() on class Bar in module Baz:

1
2
3
4
5
6
7
8
9
class InvokeMethod {
    void execute() {
        ModuleRepositoryFacade facade = new ModuleRepositoryFacade(project);
        ReloadableModule module = (ReloadableModule) (facade.getModule(module-reference/Baz/));
        Class<?> classToInvoke = module.getClass("some-package.Bar");
        Method methodToInvoke = classToInvoke.getMethod("foo");
        methodToInvoke.invoke(null);       
    }
}

Why are all the reflection tricks needed, and why not call the class directly instead? The answer is that when MPS is initialized and a project opens, it sets up classloaders and puts any dependencies module Baz might have on the classpath so that you don't have to specify them ourselves.

We still need to have the initial set of JARs on the classpath to run our class and start MPS. Here is how you would run our tool from Gradle:

1
2
3
4
5
task runCommandLineTool(type: JavaExec) { 
    main = 'CommandLineTool' 
    classpath file('solutions/commandline/classes_gen') // Location of CommandLineTool.class
    classpath fileTree("$mps_home/lib") // $mps_home points to the MPS installation
}

You can also add MPS to the Gradle dependencies block:

1
2
3
4
5
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
    implementation fileTree("$mps_home/lib")
}

Last update: July 9, 2023

Comments