grails.project.dependency.resolution = {
...
dependencies {
// Required to build/package ckeditor
build 'com.ckeditor:ckbuilder:1.6.1'
...
}
}
25 February 2014
The Grails Framework has some interesting quirks, particular when it comes to dependency management and build/deploy. With a Grails plugin, you can define dependencies in its BuildConfig.groovy
source file or you can place Java JAR files in the lib
directory of the plugin. Either will get included in the packaged plugin (and by extension, the packaged WAR of the application that includes the plugin) unless other special steps are taken to preclude them at package time. This became an issue for me
recently when using the builder provided by CKEditor within a Grails plugin to compile from source the CKEditor JavaScript files. We had originally put the CKEditor builder JAR in the lib
folder of a plugin, as the CKEditor project does not
publish its artifacts to Maven. This worked fine until we happened to update the version of Guava that the plugin depended on. The net result of this was a fun game of classpath roulette, as the builder JAR provided by CKEditor includes copies of
an older version of Guava inside its JAR (don’t get me started on this). I was able to determine this by using JBoss’s Tattletale library to check for duplicate classes on the classpath (In a future post,
I will talk about how I added JBoss’s Tattletale to our Grails builds to check for duplicate classes on the classpath, though it less important in Grails 2.3+, where you can use Maven POM files and the
enforcer plugin to do the same thing). Because we had the JAR in the lib
directory, it gets included on the classpath and packaged in the WAR, but does not get conflict resolved with any dependencies included in the BuildConfig.groovy
file. Obviously,
Ivy only knows about the dependencies declared in the closures in BuildConfig.groovy
.
I decided that the best way to fix this was to make use of the build
dependency scope supported by Grails/https://ant.apache.org/ivy/[Ivy, window="_blank"], which does not allow the dependency to be included in the packaged artifact. This first meant pushing
the CKEditor builder JAR file into our Maven repository and then adding the following to the dependencies
block in BuildConfig.groovy
:
grails.project.dependency.resolution = {
...
dependencies {
// Required to build/package ckeditor
build 'com.ckeditor:ckbuilder:1.6.1'
...
}
}
The next step was to modify the Gant script that we wrote to perform the CKEditor compilation to programmatically invoke the CKEditor builder instead of executing a command:
import ckbuilder.ckbuilder
target(packageCkeditor: "Packages the CKEditor for inclusion as a dependency.") {
String srcPath = 'web-app/js/ckeditor-source'
String destPath = 'web-app/js/ckeditor'
String version = '4.0.0'
grailsConsole.addStatus "Compiling CKEditor from source..."
ckbuilder.main(["--build", "${myPluginDir}/${srcPath}/", "${myPluginDir}/${destPath}", "--version=${version}",
"--build-config", "${myPluginDir}/${srcPath}/dev/builder/ddc-build-config.js", "--overwrite", "--skip-omitted-in-build-config"] as String[])
grailsConsole.updateStatus "Moving compiled CKEditor to target directory..."
ant.move(file: "${myPluginDir}/${destPath}/ckeditor",
tofile: "${myPluginDir}/${destPath}",
overwrite: true)
grailsConsole.updateStatus "Removing CKEditor archive artifacts..."
ant.delete(file: "${myPluginDir}/${destPath}/ckeditor_${version.toLowerCase()}.zip")
ant.delete(file: "${myPluginDir}/${destPath}/ckeditor_${version.toLowerCase()}.tar.gz")
grailsConsole.updateStatus "CKEditor successfully compiled and ready to deploy."
}
setDefaultTarget(packageCkeditor)
The example above makes use of the main()
method of the ckeditor
class provided by the builder to invoke the CKEditor compilation. I tested it out and noticed that
none of my console statements after the call to main()
were being executed. I quickly realized that something in the CKEditor code was executing System.exit()
, which in turn
ended up killing the Grails process.
To avoid this premature exit, I decided to add a temporary custom SecurityManager
that does not allow exits to happen:
import ckbuilder.ckbuilder
target(packageCkeditor: "Packages the CKEditor for inclusion as a dependency.") {
String srcPath = 'web-app/js/ckeditor-source'
String destPath = 'web-app/js/ckeditor'
String version = '4.0.0'
grailsConsole.addStatus "Compiling CKEditor from source..."
/*
* The CKEditor builder calls System.exit(). Therefore,
* we need a custom security manager to prevent it from
* killing the Grails process too. The code below
* saves off the current security manager, sets the JVM
* to use the custom no-exits-allowed manager and then
* restores it after attempting to build CKEditor.
*/
def defaultSecurityManager = System.getSecurityManager()
System.setSecurityManager(new NoExitSecurityManager())
try {
ckbuilder.main(["--build", "${myPluginDir}/${srcPath}/", "${myPluginDir}/${destPath}", "--version=${version}",
"--build-config", "${myPluginDir}/${srcPath}/dev/builder/ddc-build-config.js", "--overwrite", "--skip-omitted-in-build-config"] as String[])
} catch (e) {
if(!(e instanceof SecurityException)) {
grailsConsole.addStatus("Failed to execute CKEditor build: ${e.getMessage()}")
}
} finally {
// Restore the security manager
System.setSecurityManager(defaultSecurityManager)
}
grailsConsole.updateStatus "Moving compiled CKEditor to target directory..."
ant.move(file: "${myPluginDir}/${destPath}/ckeditor",
tofile: "${myPluginDir}/${destPath}",
overwrite: true)
grailsConsole.updateStatus "Removing CKEditor archive artifacts..."
ant.delete(file: "${myPluginDir}/${destPath}/ckeditor_${version.toLowerCase()}.zip")
ant.delete(file: "${myPluginDir}/${destPath}/ckeditor_${version.toLowerCase()}.tar.gz")
grailsConsole.updateStatus "CKEditor successfully compiled and ready to deploy."
}
setDefaultTarget(packageCkeditor)
/**
* Custom "no exits allowed" security manager implementation.
*/
class NoExitSecurityManager extends SecurityManager {
@Override
public void checkExit(int status) {
throw new SecurityException('Exit not allowed.')
}
@Override
public void checkPermission(Permission perm) {
// Allow all!
}
}
The revamped code above now uses a custom SecurityManager
that does not allow the exit to happen. While this is not the cleanest approach (I would have liked to have modified the CKEditor
builder code, but they have not open sourced the builder — only the editor itself), it gets the job done. Now, we can use the CKEditor programmatically and let Ivy manage the dependency
the dependency, ensure that it does not get included in the packaged artifact and still be able to compile the source as part of our builds.