xtext - export dsl as runnable jar

If you write an xtext-dsl you get a great editor and generator inside of eclipse. But there are times you want the generator to generate the code outside of eclipse. e.g. for using in a automated workflow.

I'm not sure what new options were introduced since XText 2.8 but I guess it is worth mentioning nonetheless.

Here are the steps to take:

  1. Make sure to add generateJavaMain = true to your DSL's mwe2-file:

    fragment = generator.GeneratorFragment auto-inject {
      generateJavaMain = true
    }

    Next time you rerun mwe2 (right-click on your mwe2-file => run-as mwe2) a file called Main.java will be generated in the Generator-Folder

  2. Create a Run-Configuration to start the generated Main.java

  3. Export your dsl as runable-jar:
  • right-click on your dsl-project => export=>Java=>Runnable Jar File
  • Select the Launch-Configuration that is starting the Main.java
  • Select the output-Folder
  • Using the Save as Ant-Script is optional but becomes handy if you are testing a lot.
    Once you have create this you can call it to pack the jar again. (It will not build, this is usually done automatically by eclipse) e.g. like this

      ant -f packjar.xml
  1. Your basically done. You can now create your generated output by calling:

      java --jar mydsl-runnable.jar input.mydsl

This will usually create the output in a src-gen-folder. If you want to override this behaviour you can use OutputConfigurations.

Optional:

  • To change the outputFolder you can change the outputPath in the Main.java:

      // configure and start the generator
      fileAccess.setOutputPath("src-gen/");
  • More advanced are output-configurations. In this you can e.g. tell the dsl's filesystem how to behave if a file is already generated. In one project I wanted some files to overwrite old versions and some files to be created once and never overwrite those (e.g. because I want the user to add their logic in there). Here is an example. You have to replace the setOutputPath-Part with those lines in Main.java:

  String outputFolder = "./out";

  OutputConfiguration defaultOutput = new OutputConfiguration(IFileSystemAccess.DEFAULT_OUTPUT);
  defaultOutput.setDescription("Output Folder");
  defaultOutput.setOutputDirectory(outputFolder+"/src-gen");
  // tell the fsa to override(!) existing resources!!
  defaultOutput.setOverrideExistingResources(true);
  defaultOutput.setCreateOutputDirectory(true);
  defaultOutput.setCleanUpDerivedResources(true);
  defaultOutput.setSetDerivedProperty(true);

  OutputConfiguration genonceOutput = new OutputConfiguration("gen-once");
  genonceOutput.setDescription("Generate Once Output Folder");
  genonceOutput.setOutputDirectory(outputFolder+"/src-nodes");
  // tell the fsa to NOT override existing resources!!
  genonceOutput.setOverrideExistingResources(false);
  genonceOutput.setCreateOutputDirectory(true);
  genonceOutput.setCleanUpDerivedResources(false);
  genonceOutput.setSetDerivedProperty(false);

  Map<String,OutputConfiguration> fsaConfs = new HashMap<>();
  fsaConfs.put(IFileSystemAccess.DEFAULT_OUTPUT, defaultOutput);
  fsaConfs.put("gen-once",genonceOutput);

  fileAccess.setOutputConfigurations(fsaConfs);

You can access the different Configurations like this:

  fsa.generateFile(String fileName,String outputConfigurationName,CharSequence contents);

Obviously this all makes only sense if you also created something like this in your eclipse-runtime.

Sry, for the redundant code here. You actually should have one solution that get's called at an appropriate time. Ok, here we go:

  • Create an xtend-class 'CustomOutputConfigurationProvider.xtend' in your dsl's main-folder with this content (make sure to adapt package and other things that might differ):
  package org.tt.gen

  import java.util.Set
  import org.eclipse.xtext.generator.OutputConfiguration
  import org.eclipse.xtext.generator.IOutputConfigurationProvider
  import org.eclipse.xtext.generator.IFileSystemAccess

  public class CustomOutputConfigurationProvider implements IOutputConfigurationProvider {

    public static final String GEN_ONCE_OUTPUT = "gen-once";

    /**
     * @return a set of {@link OutputConfiguration} available for the generator
     */
     override def Set<OutputConfiguration> getOutputConfigurations() {
      val OutputConfiguration defaultOutput = new OutputConfiguration(IFileSystemAccess.DEFAULT_OUTPUT);
      defaultOutput.setDescription("Output Folder");
      defaultOutput.setOutputDirectory("./src-gen");
      defaultOutput.setOverrideExistingResources(true);
      defaultOutput.setCreateOutputDirectory(true);
      defaultOutput.setCleanUpDerivedResources(true);
      defaultOutput.setSetDerivedProperty(true);

      val OutputConfiguration readonlyOutput = new OutputConfiguration(GEN_ONCE_OUTPUT);
      readonlyOutput.setDescription("Read-only Output Folder");
      readonlyOutput.setOutputDirectory("./src-nodes");
      readonlyOutput.setOverrideExistingResources(false);
      readonlyOutput.setCreateOutputDirectory(true);
      readonlyOutput.setCleanUpDerivedResources(false);
      readonlyOutput.setSetDerivedProperty(false);
      return newHashSet(defaultOutput, readonlyOutput);
    }
  }
  • Now we need to tell the runtime-version to use our new custom outputconfiguration-provider. (Viva dependency injection!!).
    Add following lines in your DSL's runtime-module-class' configure-method that is located in your DSL's main-folder:

      binder.bind(IOutputConfigurationProvider.class)
    .to(CustomOutputConfigurationProvider.class)
    .in(Singleton.class);

That should be it.