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
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:
<server port='8182'/> gets translated to { 'server' => { '@port' => 8182 } }
Array
on the Ruby side<includes><include>*.rb</include></includes> becomes { 'includes' => ['*.rb'] }
<arguments><argument>-classpath</argument><classpath /></arguments> becomes { 'arguments' => ['-classpath', xml('</classpath />') ] }
With this you probably can rewrite your pom.xml files as pom.rb.
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.
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).
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 offers three objects:
The artifacts (ctx.project.artifacts
) are resolved for the compile scope.
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 :)
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.
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
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
Enjoy and please report issues to help improve things or just to ask a question!
To use Maven correctly you'll need to understand the fundamentals. This class is designed to deliver just that.