Tuesday, July 14, 2009

Griffon Plugin Development

I finally got to the point in my Griffon application development that I was ready to release my app. Griffon has a plugin for creating installers and platform-specific launchers, but it didn't generate a proper Mac .app bundle. Being a Mac user myself, I really wanted to be able to deploy my app natively on Macs, so I decided to scratch my itch and add support to the plugin. I learned a fair bit about Griffon plugin development, so keep reading if you're interested in tweaking existing plugins or creating new plugins.

First off, Griffon plugins are simply Griffon applications with a few extra bits not normally used in a standard application—scripts for providing command line targets e.g., griffon create-mac-launcher, and interception of Griffon events like eventCleanEnd. To get started with plugin development, you'll want to decide whether you want to create a new plugin or edit an existing one.

Setting up your Griffon plugin development environment is fairly straightforward. If you want to edit an existing plugin:

cd MyExistingApplication
griffon install-plugin installer

If you want to create a new plugin and install it in an existing application:

griffon create-plugin MyNewPlugin
cd MyExistingApplication
{edit application.properties to include a line 'plugin.my-new-plugin=0.1'}

The real trick for both of these scenarios is to link the plugin source to somewhere where you can edit it live. This makes testing a whole lot easier. I had to pull a few strings to get this tip, but I'm going to share it with you for free:
Go into your ~/.griffon/{griffon version}/projects/MyExistingApplication/plugins/ and either edit the plugin directly or symlink it to something more convenient:

cd ~/.griffon/{griffon version}/projects/MyExistingApplication/plugins/
rm -rf installer-0.3
ln -s ~/Workspace/griffon/griffon-installer installer-0.3

You just have to make sure the name and the version match what is in your MyExistingApplication/application.properties file. If you installed an existing plugin, you should be good to go. If you're creating a new plugin, it defaults to 0.1.

Once you have your plugin development environment setup, you're ready to go to town. Below are a few things I learned along the way. Most have to do with Gant scripting, which I wasn't very familiar with before this but is easy to pick up.

If you want to create a new command line option, e.g. griffon say-hello-world you need to create a new script in your plugin. You can do this by creating a SayHelloWorld.groovy file in the $plugin/scripts. Or as Andres points out in the comments a simple griffon create-script SayHelloWorld works as well. Even better! You also have to define a sayHelloWorld target in your script and make it the default target:

target(sayHelloWorld:"Says 'Hello World'") {
// any Groovy is valid here
println "Hello World!"

Another useful feature is the ability to invoke targets in other strips, such as if you wanted to make sure the Griffon app was built before your task was executed. You can accomplish this by including the targets of other scripts in your script:

includeTargets << griffonScript("_GriffonPackage") // script name minus the .groovy

target(doAfterBuild: "Build and then run me") {
depends(checkVersion, packageApp, classpath) // defined by _GriffonPackage.groovy
packageApp() // calls the packageApp target in _GriffonPackage.groovy
println "The JARs in $basedir/staging should be fresh!"


Finally, before this post gets too long-winded, I'll cover the topic of events. Events are fired by the Griffon core runtime and by plugins in response to actions in the workflow. They provide an extension mechanism for plugins who wish to respond to various actions. Your plugin can intercept events by providing a $plugin/scripts/_Events.groovy file which declares interest in specific events:

eventCleanEnd = {
// jar
ant.delete(dir:"${basedir}/installer/jar/dist", failonerror:false)
ant.delete(dir:"${basedir}/installer/jar/classes", failonerror:false)

// jsmooth
ant.delete(dir:"${basedir}/installer/jsmooth/dist", failonerror:false)

// linux
ant.delete(dir:"${basedir}/installer/linux/dist", failonerror:false)

// mac
ant.delete(dir:"${basedir}/installer/mac/dist", failonerror:false)

// windows
ant.delete(dir:"${basedir}/installer/windows/dist", failonerror:false)

If you are developing your own plugin, you should consider publishing events so that other plugins can hook into your workflow. You can do this by including the _GriffonEvents targets in your script, or by including any script that itself includes _GriffonEvents:

includeTargets << griffonScript("_GriffonEvents")

target(myPluginTask: "") {
event("MyPluginTaskStart", [])
println "My Plugin Task!"
event("MyPluginTaskEnd", [])


Thanks for following along. Any suggestions or questions are greatly appreciated!

Oh and if you're interested in the installer plugin stuff, check out: http://dev.psicat.org/ for some example outputs. These are not the exact outputs as I am bundling two Griffon apps--the main PSICAT one and a second one in tools/--but you get the idea.


Andres Almiray said...

calling 'griffon create-script SayHelloWorld' should also work ;-)

Josh Reed said...

Sure, if you want to do it the easy way, Andres. :)

I'll update the post.