Polyglot for Maven: Ruby DSL

This is a short article on how the Ruby DSL for Polyglot for Maven came to be. When I first started with maven-1 you could script part of the build functionality inside the project.xml using jelly-script. It looked like a good idea but once your build system grew bigger it turned out that jelly-script itself was too limited as a programming language and XML is not meant to be programming language. With maven-2 the jelly-script was gone and now you could write plugins in Java. It was a relief to use a familiar programming language in your build system. But such a plugin is too far away from the actual build and it is a huge step to start your own plugin for your build system.

Polyglot for Maven offers a ground between those extremes of maven-1 and maven-2. If you write your POM with a programming language using a maven specific DSL you have already some scripting ability in place. Maven-Polyglot is written in Java and for a Ruby DSL the obvious choice is to use JRuby. JRuby itself comes with an excellent java integration, i.e. you can code java inside a ruby script !

JRuby for me brings the Java world and Ruby world together and you can pick the cherries. In the Ruby world you find some tools (rubygems, bundler, jbundler, etc) which share some aspects with Maven. Hence it is natural that the Ruby DSL for Maven-Polyglot tries to bridge between the Java and Ruby world in the spirit of JRuby.

There will be two parts

  • coming from Maven pom.xml and use ruby for scripting within the build system
  • starting with a gemspec file, Gemfile, etc and extend them to be part of Maven

pom.xml to pom.rb

The xml structure of the pom.xml can be almost translated one to one using nested blocks to a pom.rb

project do
  artifact_id 'my-project'
  group_id 'com.example'
  version '1.0.0'
  dependencies do
    dependency do
      group_id 'org.apache.maven'
      artifact_id 'maven-model'
      version '3.0.0'
    end
  end
end

Just note that camelcase names like groupId or artifactId from the XML get converted to snakecase group_id or artifact_id. This is true for all XML tags but the configuration parts of plugins !

Going one first step toward ruby side of things (more comes later) you also can write the above pom.rb as

project do
  id 'com.example:my-project', '1.0.0'
  jar 'org.apache.maven:maven-model', '3.0.0'
end

i.e. the the id is just a compact version of group_id, artifact_id and version. The jar dependency uses the same compactification as id and short cuts the type info.

Maven plugins come with a lot of custom XML as configuration. To translate this XML into a Ruby Hash following rules apply:

  • xml attributes uses the attribute name and prepend a @
<server port='8182'/> gets translated to { 'server' => { '@port' => 8182 } }
  • collections uses Array on the Ruby side
<includes><include>*.rb</include></includes> becomes { 'includes' => ['*.rb'] }
  • collections with mixed elements (exec-maven-plugin)
<arguments><argument>-classpath</argument><classpath /></arguments> becomes { 'arguments' => ['-classpath', xml('</classpath />') ] }

With this you probably can rewrite your pom.xml files as pom.rb.

Scripting

Actually there are two ways to add your scripts to the system. When the pom.rb get evaluated Maven-Polyglot builds a memory model of the POM. During that evaluation you can use any ruby to fill that DSL.

During the execution of the build you can execute some script during a given phase. This is basically writing your own plugins within the POM. The execute block will get passed in an Context object which allows you to access the MavenProject or the Logger, etc.

Example: Scripting during DSL Evaluation

Here the example shows how to fill the POM properties section with the properties from the build.properties file.

project do
  id 'com.example:my-project:1.0.0'
  props = java.util.Properties.new
  props.load( java.io.FileInputStream.new( 'build.properties' ) )
  properties props.to_hash
end

Also note that the id just has one parameter, a common maven format of writing the GAV (group_id-artifact_id-version).

Example: Execute a Script during a given Phase

This example has a gem depencency and installs it during the initialize phase with the help of JRuby. It is a bit more elaborate since it uses a Gem Artifact from rubygems-proxy.torquebox.org and installs the gem so further Ruby scripts can use that library. Gem Artifacts have all the common group-id rubygems. The execute block uses the fact that all artifacts are resolved, i.e. all transitive gems will be installed as well.

project do
  id 'com.example:my-project:1.0.0'
  repositories do
    repository do
      id 'rubygems-releases'
      url 'http://rubygems-proxy.torquebox.org/releases'
    end
  end
  dependencies do
    dependency do
      group_id 'rubygems'
      artifact_id 'maven-tools'
      version '1.0.0.rc5'
      type :gem
    end
  end
  build do
    execute( 'install-gems', :initialize ) do |ctx|
      require 'rubygems/installer'
      gem_home = File.join( ctx.project.build.directory.to_pathname, 'rubygems' )
      ctx.log.info( "install gems to #{gem_home}" )
      ctx.project.artifacts.each do |a|
        ctx.log.info( "\t#{a.artifact_id}" )
        installer = Gem::Installer.new( a.file.to_pathname,
                                        :ignore_dependencies => true,
                                        :install_dir => gem_home )
        installer.install
      end	
    end
  end
end

Here the execute block makes use of the given Context ctx to log info for the user and to access the resolved artifacts.

The Context passed to the Execution Block

The context offers three objects:

  • basedir (java.io.File)
  • project (org.apacha.maven.project.MavenProject)
  • log (org.apache.maven.plugin.logging.Log)

The artifacts (ctx.project.artifacts) are resolved for the compile scope.

Ruby and Platform Dependent Paths

In Java paths (i.e. File) are platform dependent whereas ruby internal path representation is platform independent. Especially with objects from the MavenProject (part of the context) are from the Java side of things, i.e. any path is most likely absolute and platform dependent !

To work with them inside the Ruby script all java.io.File and java.lang.String objects have singular method to_pathname returning the Ruby representation of that path (thanks to JRuby which allows to add such methods during runtime even to Java objects :)

Making the DSL even more Ruby like

In the beginning there was already the jar dependency, but this can be done for war, ear, pom or gem as well. Just try your type of dependency and see if it works, otherwise use the dependency declaration.

Artifact coordinates can split to two arguments or three arguments or can be just one. see:

jar 'org.apache.maven', 'maven-model', '3.0.1'
war 'com.example', 'myproject', '1.0.0'
pom 'org.jruby', 'jruby', '1.7.12'

or

jar 'org.apache.maven:maven-model', '3.0.1'
war 'com.example:myproject', '1.0.0'
pom 'org.jruby:jruby', '1.7.12'

or

jar 'org.apache.maven:maven-model:3.0.1'
war 'com.example:myproject:1.0.0'
pom 'org.jruby:jruby:1.7.12'

the only exception is the gem dependency where the ruby like notation is used and there is no group_id as well !

gem 'maven-tools', '1.0.0.rc5'

and you can use the usual Ruby version constraints:

gem 'maven-tools', '< 1.0.0.rc5'
gem 'maven-tools', '<= 1.0.0.rc5'
gem 'maven-tools', '> 1.0.0.rc5'
gem 'maven-tools', '>= 1.0.0.rc5'
gem 'maven-tools', '~> 1.0.0.rc5'
gem 'maven-tools', '> 1.0.0.rc5', '< 2.0.0'

Of course the Maven version ranges will work as well.

To simplify things further nested “leave” elements can be declared as an option hash instead of going into the next block level:

repository :id => 'rubygems-releases', :url => 'http://rubygems-proxy.torquebox.org/releases'

The POM XML has quite some collections elements like dependencies, repositories, plugins. You can omit them ! Decide for yourself which style is yours. Merging the above examples and simplifying the DSL you will get:

project :name => 'My Project' do
  id 'com.example:my-project', '1.0.0'
  props = java.util.Properties.new
  props.load( java.io.FileInputStream.new( 'build.properties' ) )
  properties props.to_hash
	  
  repository :id => 'rubygems-releases', :url => 'http://rubygems-proxy.torquebox.org/releases'
	  
  gem 'maven-tools', '1.0.0.rc5'

  execute( :id => 'install-gems', :phase => :initialize ) do |ctx|
    require 'rubygems/installer'
    gem_home = File.join( ctx.project.build.directory.to_pathname, 'rubygems' )
    ctx.log.info( "install gems to #{gem_home}" )
    ctx.project.artifacts.each do |a|
    ctx.log.info( "\t#{a.artifact_id}" )
      installer = Gem::Installer.new( a.file.to_pathname,
                                      :ignore_dependencies => true,
                                      :install_dir => gem_home )
      installer.install
    end
  end
end

Overall it feels more compact.

Ruby DSL POM and Maven-Central

Currently when you install or deploy a project with Ruby DSL POM then the installed POM is XML !

Sometimes it is helpful to see the XML representation of the pom.rb file. Maybe you even want to keep it around (read-only) for people or CIs which do have Maven installed but no Maven-Polyglot. To dump the XML representation of the POM you can add following properties to your POM

properties( 'tesla.dump.pom' => 'pom.xml', 'tesla.dump.readOnly' => 'true )

which will dump the pom.xml whenever the pom gets parsed. You also can trigger the dump via the commandline:

mvn validate -Dtesla.dump.pom=pom.xml

More Examples

These two examples are from the test-suite of the Ruby DSL and cover the part which is already implemented:

They both produce the same pom.xml

Meta Fu

Enjoy and please report issues to help improve things or just to ask a question!

 

Comments

Maven Training

To use Maven correctly you'll need to understand the fundamentals. This class is designed to deliver just that.

Introduction to Maven
 

Stay Connected

 

Newsletter

Subscribe to our newsletter and stay up to date with the latest news and events!