Building and running Scala programs with Gradle - 1 novembre 2010
I recently started to learn Scala, an interesting language in the (very strongly) statically typed category. So far, I only compiled single files with little or no dependency, so the command line was good enough. Now, I start to try small sample files with a rather complex classpath (using Piccolo2D and Pivot), so I searched something more sophisticated.
The main constraint was to be able to compile and run Scala programs, using my local copy of Scala compiler, and the local libraries where I gather them. I soon discovered it is now a quite unusual setup...
So, what are my options? Shell files (well, .cmd in my case, as I am on Windows) are quite limited.
Ant, I prefer to avoid. Had to use it at work, on a not-so-trivial build, and got an XML allergy... (at least when one try to program with an XML dialect! Conditionals and loops are awful!).
Same for Maven, and from what I understood, the latter imposes a given project structure, which I dislike: I like flexibility. Beside, for my simple needs, I don't really need a configuration management (yet?).
SBT (simple build tool) is praised in the Scala community, so it was a natural choice: being natively wirtten in Scala, using this language for its scripts, it seems to be just right.
So, I tried it. But whatever I tried, following the Wiki instructions, I couldn't prevent it from downloading Scala 2.7.7 (while I want to use 2.8.0) on each start. So I looked around for alternatives.
Later, I was told the sbt-launch-0.7.4.jar is only an installer... It is supposed to download and install SBT on first run, along with its dependencies, which includes the full Scala 2.7.7. It is supposed not to do that on next projects.
So I tried it again. And indeed, on a second project, it didn't download Scala 2.7.7... but installed it from its cache!
Mmm, I don't see the usefulness to have a copy of the tool on each micro-project I do (even if I see how it can be useful on bigger, shared/open source projects, perhaps using the tool at various versions), so exit for me. At least for now.
I saw good words on Buildr, but if I can avoid it, I don't want a simple tool to depend on a multi-megabyte language (OK, I made an exception for Bazaar...).
At this stage, I have spent some time to look at existing build tools. I was astonished to see the number of such tools, from makefile generators to dependency analyzers from compiler outputs (filesystem watchers), from simple one file portable scripts to big tools made only for Unix... I see lot of these tools are made in Python. Lot of them only or mostly target C or C++ languages, and using them for something else might be hard. Particularly for Java which can generate several .class files for a single .java file, and even more for Scala which can make files whose name is totally unrelated to the source files and their path...
I saw some tools are just a thin wrapper of an interpreted language (Ruby, Python...), accessing the Ant library, getting the power of the tool without the irky language. I briefly thought I could make a binding of Ant libraries with LuaJ (because it makes easy to call Java stuff), calling the result Luant for example... It is doable, but it would need some work (thus time...) and I wanted a quick result, so despite the interest of the project, I dropped the idea. For now...
Soo, I bite the bullet and went back to test Buildr... A quick look at the docs, it looks nice.
I have to install it, so I follow the instructions. First, I went to http://jruby.org/ (I use this one, supposed to be faster than plain Ruby, and at least I remain on the JVM ecosystem...) and clicked on the download button. Bam, a 19MB (expanding to 24MB) file on my disk!
After installation, I try to get Buildr:
> jruby -S gem install buildr JRuby limited openssl loaded. http://jruby.org/openssl gem install jruby-openssl for full support. ERROR: Error installing buildr: buildr requires builder (= 2.1.2, runtime)
Grr, that's not a good start. Let's try to be smart:
> jruby -S gem install builder > jruby -S gem install buildr JRuby limited openssl loaded. http://jruby.org/openssl gem install jruby-openssl for full support. ERROR: Error installing buildr: buildr requires net-ssh (= 2.0.23, runtime)
I went on, installing various packages are they were reported missing, slow and tedious process. Until it complained because I installed the newest version of a library and it wanted an older one. I gave up at this stage...
Then an article on Hudson project going from Maven to Gradle reminded this tool. I went to the site, saw it can be used to compile Java and Scala, among other languages, that's nice. Its scripts are based on Groovy, yet another language to install and (more or less) learn. Oh well. The doc seems nice.
So, I download a 22MB archive (v.0.8, I go for the stable package) expanding to a 42MB folder...
At least, 14MB of this is for doc, which is good sign.
And 20MB is for the jar files, as it appears that Gradle has lot of dependencies... At least, it groups them in the same place, no pain to get parts like I had with Buildr. Even Groovy comes bundled with the package.
Update: after some days of use, I saw lot of people already switched to the beta 0.9 version, so I started to use the 0.9-rc-2 version, which is probably stable enough for my humble needs...
For the record: archive is 34MB, unzipping to 49MB. Inflation mostly come from the libraries.
Among interesting improvements, we have incremental building and colorful console output...
So, I go to the command line and add Gradle's bat file to the path:
> set path=%path%;C:\Java\gradle-0.8\bin > gradle -v ------------------------------------------------------------ Gradle 0.8 ------------------------------------------------------------ Gradle buildtime: Monday, September 28, 2009 2:01:59 PM CEST Groovy: 1.6.4 Ant: Apache Ant version 1.7.0 compiled on December 13 2006 Ivy: 2.1.0-rc2 Java: 1.6.0_18 JVM: 16.0-b13 JVM Vendor: Sun Microsystems Inc. OS Name: Windows XP
Good.
I follow the simple tutorial, playing with tasks and their dependencies. Not much work done, but the syntax is pleasant.
I am trying to build a simple Scala project with a single source file and no dependencies yet, beside the Scala library. I don't want to download these from some repository, just use my installed jars. I will care for proper dependencies management if I make a project to share officially.
I found a useful thread showing how to use local resources: Compile using jars from a lib directory.
I end up with a working project:
group = 'org.philhosoft' mainClass = "${group}.experiments.swing.HelloWorldSwing" version = '1.0' if (!System.properties.'release') { // Using gradle -Drelease=true xxxTask version += '-SNAPSHOT' } // Get access to environment variables env = System.getenv() // Like this: scalaHome = env['SCALA_HOME'] // Grab all jars in Scala's lib dir scalaTree = fileTree(dir: "$scalaHome/lib", includes: [ '*.jar' ]) // To compile Scala // usePlugin 'scala' // v.0.8 apply plugin: 'scala' // v0.9 // Calling gradle without parameter, it will compile the project // Must be "compileScala" otherwise if using just "compile", it complains as being ambiguous // (I might want to compile Java classes within the project.) defaultTasks 'compileScala'
So far, not too hard to understand... I probably do lot of things redundant with default Gradle behavior (which seems to do "the right thing" by default) but it is a build file for experimentations, testing changes from these defaults.
So, let's define my local repository.
// Tell the dependency management where to find the libraries repositories { flatDir name: 'localRepository', dirs: 'C:/Java/libraries' }
Then I define the dependencies, particularly to define where to find Scala tools:
dependencies { // Declares scalaTools so that Gradle will know how to compile Scala code scalaTools scalaTree //~ scalaTools fileTree(dir: "$scalaHome/lib").matching { include '*.jar' } // Libraries to use when compiling (could have added scalaTree to repositories, I suppose) compile scalaTree // Just declare the name of the library, it knows where to find it from the 'repositories' part compile name: 'jyaml-1.3' // Name of the jar, without the extension // Libraries to use when running -- actually not necessary as it inherits from compile dependencies. // It is probably more used to declare additional dependencies (like a DB driver, loaded by reflection). runtime scalaTree runtime name: 'jyaml', version: '1.3' // Alternative syntax }
And I define a custom (yet classical) directory structure:
// Custom directory layout sourceSets { main { scala { srcDir 'src/scala' } } test { scala { srcDir 'test/scala' } } } // Build dir is the same for all source sets, which makes sense, I suppose buildDir = 'bin' // Change from default 'build' folder -- in 0.8, was buildDirName
The compiled files will go to bin/classes/main.
It works fine! I was able to build my simple Scala-Swing program.
Now, to run it with Gradle.
I searched around, and if I found ant.java() to run a Java class, I could find nothing equivalent for Scala.
So I looked at scala.bat, and found out it was just running it with java.exe. Aah...
I first tried to run it via scala.tools.nsc.MainGenericRunner as done in the bat file. But it was lost, couldn't find my package. Perhaps because I defined the class folder as relative, and it was running from the Scala install dir, or something like that.
Anyway, looking at this MainGenericRunner source, I found out it wasn't really needed, as it is mostly used to run the interpreter mode or the REPL, or with plain Java, depending on context. So I skipped it altogether, making a simpler run config.:
task run << { ant.java( classname: mainClass, failOnError: 'true', fork: 'true' ) { jvmarg(value: '-Xmx256M') jvmarg(value: '-Xms32M') jvmarg(value: "-Dscala.home=$scalaHome") arg(value: '42') // I don't use these args, actually... arg(value: 'foo') classpath { // My .class files pathElement(location: 'bin/classes/main') // The runtime class path defined in the configurations pathElement(path: configurations.runtime.asPath) } } }
To explore a bit more the capabilities of Gradle, I made two packaging tasks:
// Will create a jar file using the project name and the 'version' property to make the name // The file goes to $buildDir/libs task makeJar(type: Jar, dependsOn: compileScala) { from "$buildDir/classes/main" // Change base name from project name baseName = 'gradleTest' manifest { attributes( 'Main-Class': mainClass, 'Version': version, 'Class-Path': 'scala-library.jar scala-swing.jar jyaml-1.3.jar' ) } } // Zip the sources task zipSrc(type: Zip) { // Zip goes into $buildDir/distributions if (!System.properties['release']) { // Specify full name // Can also use 'appendix' (eg. 'core') and 'classifier' (eg. 'src') properties archiveName = "SomethingElse-${version}-src.zip" } // Else, use project name + version from(sourceSets.main.scala.srcDirs) { into archivesBaseName fileMode = 0755 include '**/*.scala' } }
So, after looking around some information (I haven't fully read the manual yet...), I was quite quickly able to get the result I wanted. I appreciate that I can drop a build.gradle file on an existing project, adjust some settings, and get what I want.
I contrast that with Maven or even SBT which impose (at least by default) a project structure, want to manage dependencies themselves (hard to get the hand), and so on.
I don't discard this approach, very nice for a new project (quick setup on a logical structure) and to share it (one can find quickly its way in a random project). I can also see the interest of defining resources (libraries) on a repository and getting them from there: there is no need to hunt them on the Net, risking to get a newer version that could be incompatible.
But I don't believe in "one size fits all": sometime we have to work on a legacy project, I want to make lot of little unrelated projects without wanting to have a full copy of my libraries on each (I am a dinosaur, coming from a world where hard disk space was spare... ).
I found Gadle easy to use, yet apparently powerful, making me able to reuse my Ant knowledge, flexible and not in the way. If, later, I want to be nice to others and use dependency management, I can do that (eg. using Ivy or Maven stuff). So far, I am sold on this tool.
In the Maven vs. Gradle vs. Ant article, we have a nice interview of Hans Dockter, the main man behind Gradle, explaining the philosophy behind the tool. I particularly appreciate the quotation of Erich Gamma against Frameworkitis, meeting my thoughts here...
Update 2: I tried with Maven-style build. As expected, much longer (see below), but of course, that's only the first time, it goes into a cache. And, unlike what I feared (from my SBT experience), the downloaded jars just remain in the cache, there is no copy per project. Wew!
> gradle -PuseMavenStyle Using Maven-style dependencies :compileJava Download http://repo1.maven.org/maven2/org/scala-lang/scala-library/2.8.0/scala-library-2.8.0.pom Download http://repo1.maven.org/maven2/org/scala-lang/scala-swing/2.8.0/scala-swing-2.8.0.pom Download http://repo1.maven.org/maven2/org/jyaml/jyaml/1.3/jyaml-1.3.pom Download http://repo1.maven.org/maven2/org/scala-lang/scala-library/2.8.0/scala-library-2.8.0.jar Download http://repo1.maven.org/maven2/org/scala-lang/scala-swing/2.8.0/scala-swing-2.8.0.jar Download http://repo1.maven.org/maven2/org/jyaml/jyaml/1.3/jyaml-1.3.jar :compileScala Download http://repo1.maven.org/maven2/org/scala-lang/scala-compiler/2.8.0/scala-compiler-2.8.0.pom Download http://repo1.maven.org/maven2/org/scala-lang/scala-compiler/2.8.0/scala-compiler-2.8.0.jar BUILD SUCCESSFUL Total time: 4 mins 7.986 secs
// Testing project properties // Set this one with gradle -PuseMavenStyle useMavenStyle = hasProperty('useMavenStyle') if (useMavenStyle) { println 'Using Maven-style dependencies' } // Tell the dependency management where to find the libraries repositories { if (useMavenStyle) { // Classical form: online mavenCentral() // Can probably be omitted, as it is already in mavenCentral mavenRepo urls: 'http://scala-tools.org/repo-releases/' } else { // My way: use the stuff I downloaded manually! flatDir name: 'localRepository', dirs: 'C:/Java/libraries' } } dependencies { // Declares scalaTools so that Gradle will know how to compile Scala code if (useMavenStyle) { // Libraries needed to run the Scala tools scalaTools 'org.scala-lang:scala-compiler:2.8.0' scalaTools 'org.scala-lang:scala-library:2.8.0' // Libraries needed for Scala API compile 'org.scala-lang:scala-library:2.8.0' compile 'org.scala-lang:scala-swing:2.8.0' compile group: 'org.jyaml', name: 'jyaml', version: '1.3' } else { scalaTools scalaTree //~ scalaTools fileTree(dir: "$scalaHome/lib").matching { include '*.jar' } // Libraries to use when compiling compile scalaTree // Just declare the name of the library, it knows where to find it from the 'repositories' part compile name: 'jyaml-1.3' // Name of the jar, without the extension } // Libraries to use when running -- actually not necessary as it inherits from compile dependencies // It is probably more used to declare additional dependencies (like a DB driver, loaded by reflection). //~ runtime scalaTree runtime name: 'jyaml', version: '1.3' // Alternative syntax }
Update 1: 2010-11-04 - Using Gradle 0.9-rc-2 and more experiments.
Update 2: 2010-11-04 - Testing Maven-like dependencies.