h1. Extending Buildr

h2. Organizing Tasks

A couple of things we learned while working on Buildr.  Being able to write
your own Rake tasks is a very powerful feature.  But if you find yourself
doing the same thing over and over, you might also want to consider functions.
They give you a lot more power and easy abstractions.

For example, we use OpenJPA in several projects.  It's a very short task, but
each time I have to go back to the OpenJPA documentation to figure out how to
set the Ant MappingTool task, tell Ant how to define it.  After the second
time, you're recognizing a pattern and it's just easier to write a function
that does all that for you.

Compare this:

{{{!ruby
file('derby.sql') do
  REQUIRES = [
    'org.apache.openjpa:openjpa-all:jar:0.9.7-incubating',
    'commons-collections:commons-collections:jar:3.1',
    . . . 
    'net.sourceforge.serp:serp:jar:1.11.0' ]
  ant('openjpa') do |ant|
    ant.taskdef :name=>'mapping',
      :classname=>'org.apache.openjpa.jdbc.ant.MappingToolTask',
      :classpath=>REQUIRES.join(File::PATH_SEPARATOR)
    ant.mapping :schemaAction=>'build', :sqlFile=>task.name,
      :ignoreErrors=>true do
        ant.config :propertiesFile=>_(:source, :main, :sql, 'derby.xml')
        ant.classpath :path=>projects('store', 'utils' ).
          flatten.map(&:to_s).join(File::PATH_SEPARATOR)
    end
  end
end
}}}

To this:

{{{!ruby
file('derby.sql') do
  mapping_tool :action=>'build', :sql=>task.name,
    :properties=>_(:source, :main, :sql, 'derby.xml'),
    :classpath=>projects('store', 'utils')
end
}}}

I prefer the second.  It's easier to look at the Buildfile and understand what
it does.  It's easier to maintain when you only have to look at the important
information.

But just using functions is not always enough.  You end up with a Buildfile
containing a lot of code that clearly doesn't belong there.  For starters, I
recommend putting it in the @tasks@ directory.  Write it into a file with a
@.rake@ extension and place that in the @tasks@ directory next to the
Buildfile.  Buildr will automatically pick it up and load it for you.

If you want to share these pre-canned definitions between projects, you have a
few more options.  You can share the @tasks@ directory using SVN externals.
Another mechanism with better version control is to package all these tasks,
functions and modules into a "Gem":http://rubygems.org/ and require it from
your Buildfile.  You can run your own internal Gem server for that.

For individual task files, you can also use
"Sake":http://errtheblog.com/post/6069 for system-wide Rake tasks deployment.


h2.  Creating Extensions

The basic mechanism for extending projects in Buildr are Ruby modules.  In
fact, base features like compiling and testing are all developed in the form
of modules, and then added to the core Project class.
  
A module defines instance methods that are then mixed into the project and
become instance methods of the project.  There are two general ways for
extending projects.  You can extend all projects by including the module in
Project:

{{{!ruby
class Project
  include MyExtension
end
}}}

You can also extend a given project instance and only that instance by
extending it with the module:

{{{!ruby
define 'foo' do
  extend MyExtension
end
}}}

Some extensions require tighter integration with the project, specifically for
setting up tasks and properties, or for configuring tasks based on the project
definition.  You can do that by adding callbacks to the process.

The easiest way to add callbacks is by incorporating the Extension module in
your own extension, and using the various class methods to define callback
behavior.

|_. Method        |_. Usage |
| @first_time@    | This block will be called once for any particular
extension.  You can use this to setup top-level and local tasks. |
| @before_define@ | This block is called once for the project with the project
instance, right before running the project definition.  You can use this to add
tasks and set properties that will be used in the project definition. |
| @after_define@  | This block is called once for the project with the project
instance, right after running the project definition.  You can use this to do
any post-processing that depends on the project definition. |

This example illustrates how to write a simple extension:

{{{!ruby
module LinesOfCode
  include Extension

  first_time do
    # Define task not specific to any projet.
    desc 'Count lines of code in current project'
    Project.local_task('loc')
  end

  before_define do |project|
    # Define the loc task for this particular project.
    define_task 'loc' do |task|
      lines = task.prerequisites.map { |path| Dir["#{path}/**/*"] }.flatten.uniq.
        inject(0) { |total, file| total + File.readlines(file).count }
      puts "Project #{project.name} has #{lines} lines of code"
    end
  end

  after_define do |project|
    # Now that we know all the source directories, add them.
    task('loc'=>compile.sources + compile.test.sources)
  end

  # To use this method in your project:
  #   loc path_1, path_2
  def loc(*paths)
    task('loc'=>paths)
  end

end

class Buildr::Project
  include LinesOfCode
end
}}}


h2.  Using Alternative Layouts

Buildr follows a common convention for project layouts: Java source files
appear in @src/main/java@ and compile to @target/classes@, resources are
copied over from @src/main/resources@ and so forth.  Not all projects follow
this convention, so it's now possible to specify an alternative project
layout.

The default layout is available in @Layout.default@, and all projects inherit
it.  You can set @Layout.default@ to your own layout, or define a project with
a given layout (recommended) by setting the @:layout@ property.  Projects
inherit the layout from their parent projects.  For example:

{{{!ruby
define 'foo', :layout=>my_layout do
  ...
end
}}}

A layout is an object that implements the @expand@ method.  The easiest way to
define a custom layout is to create a new @Layout@ object and specify mapping
between names used by Buildr and actual paths within the project.  For
example:

{{{!ruby
my_layout = Layout.new
my_layout[:source, :main, :java] = 'java'
my_layout[:source, :main, :resources] = 'resources'
}}}

Partial expansion also works, so you can specify the above layout using:

{{{!ruby
my_layout = Layout.new
my_layout[:source, :main] = ''
}}}

If you need anything more complex, you can always subclass @Layout@ and add
special handling in the @expand@ method, you'll find one such example in the
API documentation.

The built-in tasks expand lists of symbols into relative paths, using the
following convention:

|_. Path                          |_. Expands to |
| @:source, :main, <lang/usage>@  |  Directory containing source files for a
given language or usage, for example, @:java@, @:resources@, @:webapp@. |
| @:source, :test, <lang/usage>@  | Directory containing test files for a given
language or usage, for example, @:java@, @:resources@. |
| @:target, :generated@           | Target directory for generated code
(typically source code). |
| @:target, :main, <lang/usage>@  | Target directory for compiled code, for
example, @:classes@, @:resources@. |
| @:target, :test, <lang/usage>@  | Target directory for compile test cases,
for example, @:classes@, @:resources@. |
| @:reports, <framework/usage>@   | Target directory for generated reports, for
example, @:junit@, @:coverage@. |

All tasks are encouraged to use the same convention, and whenever possible, we
recommend using the project's @path_to@ method to expand a list of symbols
into a path, or use the appropriate path when available.  For example:

{{{!ruby
define 'bad' do
  # This may not be the real target.
  puts 'Compiling to ' + path_to('target/classes')
  # This will break with different layouts.
  package(:jar).include 'src/main/etc/*'
end

define 'good' do
  # This is always the compiler's target.
  puts 'Compiling to ' + compile.target.to_s
  # This will work with different layouts.
  package(:jar).include path_to(:source, :main, :etc, '*')
end
}}}
