One Task to Bind Them All
Now that we have proper scripts in all the modules with appropriate tasks, it would be nice if we could call them all in just one task execution. The preceding list of commands seems to be a lot of work. You need to go into each directory and execute the appropriate task with Rake. For now, we have only three modules, but just imagine a project with ten or twenty modules! A simple build could require quite a lot of typing.
We're going to create a real rakefile in the project root directory: coconut. This rakefile will contain all the tasks used to execute module tasks. Let's see this file's content.
MODULES = ['persistence', 'business', 'web'] task 'build' do
MODULES.each do |m| puts "### Building #{m}" Dir.chdir(m) do load 'rakefile'
Rake::Task["#{m}.gem"].invoke end end end
Only a few lines are required, but there's a lot of Ruby here. Let's go over it so that you can fully understand what this file is doing.
The first thing to notice is that something is missing. Raven is not required here. We're using a simple task definition—the most basic Rake task that doesn't do anything special except what you tell it to do.
The script first defines the list of modules to build. But why did I use MODULES instead of modules? Just for the pleasure of it? Not quite. In Ruby, variable names starting with an uppercase letter are considered as constants, and if you try to redefine a constant value, the Ruby interpreter will issue a warning. This way, if someone tries to update the module list, you will know about it.
Then comes the definition of the task itself, named (very originally) build. The each method is an iteration method available for all arrays in Ruby. It loops over the provided block of code, passing each element of the array in the variable defined between pipe symbols (|). That's the |m| part. So within the code block, m will successively take each value contained in the MODULES array .
The puts command just writes the name of the module that is going to be built.
Dir.chdir(m) changes the current directory to the provided value—here, the directory for each module we iterate on. The nice thing about this method is that it accepts a code block, and the directory change will be effective only for that block. When the end of the block is reached, the original directory is restored.
GOOD VERSION NUMBERS
You will occasionally find projects using all kind of versioning policies. You may see a version "number" like 4.0.5-GA, 0.5-dev-2, or an even more creative one. A good version number should be both easy to understand by a human and easily parsable by a computer, to be able to compute version orders, for example.
The RubyGems project has therefore defined a "rational" versioning policy that you should try to follow. Here's a summary of the rules:
- Versions should be represented by three integers, separated by periods (for example, 1.2.3). The first integer is the major version number, the second is the minor version number, and the third is the build number.
- A change in the implementation detail should increment the build number.
- A change that doesn't break compatibility with earlier versions should increment the minor version number and reset the build number.
- An incompatible change (like a public API modification) should increment the major build number and reset the minor and build numbers.
- Any public release should have a different version number, which usually means incrementing the build number. So developers can continue to generate builds themselves without changing the version, but as soon as they make a public release, the version should be updated.
For more information about the rational versioning policy, check the RubyGems documentation at http ://docs.ruby gems. org/read/chapter/7.
And we've finally reached the most interesting part of the script. Within a module directory, we're loading its rakefile, which means that all the task definitions for that module will be loaded. Once all the module tasks are available, one of them can be directly called—here, the module-specific gem task. Rake gives us a nice way to retrieve task definitions from their names using Rake::Task[taskname\. This gives us a Rake task object, and calling the invoke method on it will trigger the task execution. So these lines are explicitly calling a Rake task, forcing its execution.
To sum up, we've defined a new build task within the root rakefile. It iterates on all the modules, loads their respective rakefile, and executes their gem installation task. After the whole execution, we have a full project build. Let's see what the output looks like:
- gt; rake build
- Building persistence Building path src/main/java javac -classpath "target/classes" -sourcepath "src/main/java"
- d target/classes src/main/java/p/*.java Module in jar persistence Built jar file persistence.jar. Wrapping jar in a Gem Successfully built Ruby Gem Name: coconut-persistence Version: 1.0
File: coconut-persistence-1.0-java.gem ### Building business
Using local gem coconut-persistence (1.0) to satisfy dependency coconut-persistence Building path src/main/java javac -classpath "~/.raven/gems/coconut-persistence-1.0-java/ext/persistence.jar :target/classes" -sourcepath "src/main/java" -d target/classes src/main/java/b/*.java Module in jar business Built jar file business.jar. Wrapping jar in a Gem Successfully built Ruby Gem Name: coconut-business Version: 1.0
File: coconut-business-1.0-java.gem
Using local gem coconut-business (1.0) to satisfy dependency coconut-business Building path src/main/java javac -classpath "~/.raven/gems/coconut-business-1.0-java/ext/business.jar :target/classes" -sourcepath "src/main/java" -d target/classes src/main/java/w/*.java Module in jar web Built jar file web.jar. Wrapping jar in a Gem Successfully built Ruby Gem Name: coconut-web Version: 1.0
File: coconut-web-1.0-java.gem
An important point is that we want our build to be done in the right order. For example, the web module wouldn't compile if the business module hadn't been compiled beforehand. It's your responsibility to make sure that the MODULES array in the root rakefile is ordered in the correct way. Other tools just let you define dependencies between modules and then compute the build order. But in the end, it's the same: you need to give the basic dependency information. Generalizing the build task to anything of your liking shouldn't be hard now that you understand the principle of iterating on modules and calling their tasks, since it's only more of the same. Here, we're iterating on all our modules, but you could also imagine tasks working on a subset, building only certain modules, for example. This base gives you a lot of flexibility.
Post a comment