Saturday, October 22, 2011

Buildr

(earlier posted on the Avega Group Blog)
Buildr is a build tool for building Java projects designed to be a drop in replacement for Maven. It uses the same default directory structure as Maven with source files in src/main/java, tests in test/main/java, etc. And it downloads dependencies from maven repositories. So why would you want to learn yet another build tool when the two standard ones (ant and maven) are so dominating? For me it was because I was tired of writing xml files and wanted a regular programming language for specifying our builds. Buildr is built on top of Rake "the ruby make", and is a internal ruby dsl. Rake integrates nicely with ant and can call ant tasks, see this blog for an example of how to translate an existing ant file to Rake. To get a feeling for how buildr works we will walk through how to build a sample Java project with Buildr. We will use an EJB 2 application from sun oracle called Duke's Bank. I modified it slightly to get the different parts of the application in separate sub-projects. The application has a simple web gui, some ejbs and backend code and a standalone application client. You can download the sample application code with a working Buildr buildfile (named buildfile.demo, rename it buildfile if you don't want to write your own following the instructions in this blog) from my github repository. I have organized the code in four project folders, AppClient, BankEar, BankEjb and BankWeb. To get started install Buildr by following the installation instructions for your platform on Buildr's home page: http://buildr.apache.org/installing.html Open a terminal window / command prompt and cd to the directory containing the source code for the project. Run the buildr command and you should get a message from Buildr.
To use Buildr you need a buildfile. Do you want me to create one?:
1. From directory structure
2. Cancel
?  
Select 1 to let Buildr generate a buildfile for you. This will create a new file called buildfile in your current directory that looks something like this:
# Generated by Buildr 1.4.5, change to your liking
# Version number for this release
VERSION_NUMBER = "1.0.0"
# Group identifier for your projects
GROUP = "bank"
COPYRIGHT = ""

# Specify Maven 2.0 remote repositories here, like this:
repositories.remote << "http://www.ibiblio.org/maven2/"

desc "The Bank project"
define "bank" do

  project.version = VERSION_NUMBER
  project.group = GROUP
  manifest["Implementation-Vendor"] = COPYRIGHT

  define "AppClient" do
  end

  define "BankEjb" do
  end

  define "BankWeb" do
  end

end
From the buildfile we can see that each of our sub-projects gets a 'define' block, except for the 'BankEar' project that does not contain any code, but we will manually add a define block for it later. Following Maven conventions Buildr expects the source code to be located in src/main/java, but the sample application does not follow this convention. So if we run buildr in the top directory, now with the generated buildfile in place, Buildr will not find anything to compile and will just exit saying everything went well. To tell Buildr that we have a non-default project layout we define a Layout before the 'define "bank"' line and specify the location of our source code, like so:
ejb_layout = Layout.new
ejb_layout[:source, :main, :java] = 'src'

We then tell Buildr to use this layout for the BankEjb project:

define "BankeEjb", :layout=>ejb_layout
  package(:jar)
end
We also added a line to tell Buildr to package the BankEjb project as a jar file. If we run buildr again we can see that it now finds our source code and tries to compile it. The problem is just that it is missing some necessary jar dependencies. We can either add these jar files as maven style dependencies with group-id, artifact-id and version, for example to add a compile time dependency to hamcrest-core 1.1 we can add the following line to our BankeEjb project:
compile.with 'org.hamcrest:hamcrest-core:jar:1.1'
When Buildr sees this it will try to download the jar from the list of maven repositories specified in our buildfile. The other option is to add a reference to an existing jar file, or directory of jar files, on disk. Which is the approach we will use in this example:
compile.with Dir[path_to('lib/*.jar')]
This will compile the BankEjb project with all jar files in the lib/ directory. If we run buildr again it now compiles the code without errors. Similar to maven Buildr has different tasks we can run, the default being 'build':
You can also see the available tasks with 'buildr help:tasks', and the projects Buildr knows about with 'buildr help:projects'. So by just running Buildr the build task is run. To also have the BankEjb jar file created we have to run the package task with, 'buildr package'. The generated jar file will be placed in the 'target/main' directory. To continue we add a layout for the BankWeb project, specifying both a source/main/java directory as well as a source/main/webapp directory. We also add a package(:war) directive since we want Buildr to package this as a war file. If we run 'buildr clean package' we get some errors in the output telling us that the web project depends on the ejb project. So lets add it as a dependency. We do this like with did in the ejb project for external jar dependencies.
compile.with project('BankEjb')
We can also see that the web project depends on the same jars as the ejb project, to add them we tell Buildr to use the compile dependencies from BankEjb when compiling BankWeb:
compile.with project('BankEjb'), project('BankEjb').compile.dependencies
With this in place both projects compiles without errors. If we look in the src folder of the BankWeb project we will find three properties files that are not included in the generated war file. (You can check by unziping the generated war file located in BankWeb/target/main). To get the properties files packaged in the war we have to tell Buildr about them.
package(:war).include path_to(:source, :main, :java, '/**/*properties'), :path=>'WEB-INF/classes'
The :path argument is the location inside the war file where we want the properties files copied to. path_to is a Buildr function that expands its argument to a full path. The process is similar for the AppClient project and the relevant part of the resulting buildfile looks like this.
define "AppClient", :layout=>ejb_layout do
    compile.with project('BankEjb'), project('BankEjb').compile.dependencies
    package(:jar).include path_to(:source, :main, :java, '/**/*properties'), :path=>'appclient'
  end
Finally we want to add a project definition for the ear project and package the three previous projects in it.
define 'BankEar'
  package(:ear).add :jar=>project('AppLicent')
  package(:ear).add :ejb=>project('BankEjb')
  package(:ear).add :war=>project('BankWeb'), :context_root=>'bank'
  package(:ear).include path_to('conf/*'), :path=>''
end
You can read more about Buildr's ear package here. You can also see that we added a line to include the configuration files in the conf/ directory. The only thing left now is to set the display name for the application and to define some security roles.
  package(:ear).display_name = 'JBossDukesBank'
  package(:ear).security_roles << {:name=>'BankAdmin'}
  package(:ear).security_roles << {:name=>'BankCustomer'}
That's it. The resulting buildfile looks like this.
VERSION_NUMBER = "1.0.0"
# Group identifier for your projects
GROUP = "bank"
COPYRIGHT = ""

# Specify Maven 2.0 remote repositories here, like this:
repositories.remote << "http://www.ibiblio.org/maven2/"

ejb_layout = Layout.new
ejb_layout[:source, :main, :java] = 'src'

web_layout = Layout.new
web_layout[:source, :main, :java] = 'src'
web_layout[:source, :main, :webapp] = 'WebContent'

desc "The Bank project"
define "bank" do

  project.version = VERSION_NUMBER
  project.group = GROUP
  manifest["Implementation-Vendor"] = COPYRIGHT

  define "BankEar" do
    package(:ear).add :jar=>project('AppClient'), :path=>''
    package(:ear).add :ejb=>project('BankEjb'), :path=>''
    package(:ear).add :war=>project('BankWeb'), :path=>'', :context_root=>'bank'
    package(:ear).include(path_to('conf/*'), :path=>'')
    package(:ear).display_name = 'JBossDukesBank'
    package(:ear).security_roles << {:name=>'BankAdmin', :id=>"admin", :description=>"Administrator role"}
    package(:ear).security_roles << {:name=>'BankCustomer', :id=>"customer", :description=>"Customer role"}
  end

  define "AppClient", :layout=>ejb_layout do
    compile.with project('BankEjb'), project('BankEjb').compile.dependencies
    package(:jar).include path_to(:source, :main, :java, '/**/*properties'), :path=>'appclient'
  end

  define "BankEjb", :layout=>ejb_layout do
    compile.with Dir[path_to('lib/*.jar')]
    package(:jar)
  end

  define "BankWeb", :layout=>web_layout do
    compile.with project('BankEjb'), project('BankEjb').compile.dependencies
    package(:war).include path_to(:source, :main, :java, '/**/*properties'), :path=>'WEB-INF/classes'
    package(:war).with :libs=>path_to(:source, :main, :webapp, 'WEB-INF/lib/*')
  end

end
If you want to deploy and run the application we first have to create the database schema. We can add a custom task for this in our buildfile. The necessary sql scripts to create the database and populate it with some sample data are located in the sql directory. We begin by adding a task to create the database using the hsql ScriptTool. Buildr tasks are defined with the Rake task keyword.
task :create_db do
    system "java -cp sql/hsqldb.jar org.hsqldb.util.ScriptTool -url jdbc:hsqldb:hsql: -database //localhost:1701 -script sql/hsql-create-table.sql"
end
system is a ruby function that runs an external program, in this case java. The create_db task creates the database. To populate it with data we need to run the hsql-insert.sql script with the hsql ScriptTool. Lets at the same time extract the system call to a ruby function.
task :create_db do
  run_hsql_script('sql/hsql-create-table.sql')
end

task :populate_db do
  run_hsql_script('sql/hsql-insert.sql')
end

def run_hsql_script(script)
  system "java -cp sql/hsqldb.jar org.hsqldb.util.ScriptTool -url jdbc:hsqldb:hsql: -database //localhost:1701 -script #{script}"
end
For this to work we need to have a hsql database running on localhost listening on port 1701. JBoss can be configured to start a hsql database upon startup. The sample application we are using is tested and works with JBoss 4.0.5, download it here. To configure hsql modify the server/default/deploy/hsqldb-ds.xml file and make sure the lines where hsql is configured to run in server mode, and can be access over tcp, are uncommented (3 places needs to be changed). If we start JBoss with the bin/run.sh script we can now run the create_db and populate_db tasks:
> buildr bank:create_db bank_populate_db
Finally if we want to deploy the generated ear file we need to copy it to the JBoss deploy dir (/server/default/deploy/), either manually or with a buildr task. You will find a task for this in the buildfile.demo file. This step by step guide has been an attempt to show how easy it is to start using Buildr to build your application. Take a look at the Buildr documentation at buildr.apache.org for a complete reference of Buildr's features. Unfortunately development seems to have stopped on Buildr the last couple of months. And nothing much is happening on the mailing lists either. So we will see what happens. If development doesn't start again soon Gradle might be a better choice if you want to use a regular programming language to build your application. At the moment there is much more activity going on there. Read more about it here.

2 comments:

slatemine said...

Good post - I fear buildr's days are numbers. I've fought with mvn compliant transitive dependency compatibility for a couple of days.

The is a lot right with buildr, projects can be specified simply and elegantly but without correct and up-to-date maven dependency management I think it is not a long term contender.

Things I like:
* Simple build scripts
* Elegant specification of project settings, including proxies via yaml
* Running on jRuby which means I can check the whole build system gems and all into source control, no more plugin not installed for the team head scratching and needing internet connections to build code.

I hope the build team have enjoyed a good summer rather than have lost momentum.

p

Mikael Amborn said...

Thank you.
I totally agree with you about the benefits of Buildr, but unfortunately development of Buildr seems to be quite slow.
I guess you know about this thread on the mailing list that discuses transitive dependencies.