Writing Modular NAnt Build Scripts
I've been quite happy with NAnt as the tool for writing build scripts. Though, once these scripts reach a certain size, they become harder and harder to maintain. The main reason for that is a lack of modularity. A common example when this becomes an issue are targets which perform their task based on an input property:
<target name="hello">
<property name="hello.name" value="World"
unless="${property::exists('hello.name')}" />
<echo message="Hello, ${hello.name}." />
</target>
The above target will behave differently based on the current value of hello.name
property. It even provides a default value to be used when the property hasn't been defined before calling the target. This looks like a nice pattern, but since all properties are global, setting the input property for this target sets it for every other target depending on it and also for all future calls, unless you explicitly set it to a different value once the call returns.
<target name="wrapper">
<call target="hello" />
<property name="hello.name" value="Europe" />
<call target="hello" />
<call target="hello" />
<property name="hello.name" value="Slovenia" />
<call target="hello" />
<call target="hello" />
</target>
The above target will output the following messages:
Hello, World.
Hello, Europe.
Hello, Europe.
Hello, Slovenia.
Hello, Slovenia.
You can't even clear the property once it has been set. In larger scripts this behavior can make maintenance unnecessarily difficult since you can never really be sure your change won't effect something else unless you thoroughly inspect and test the complete script. Being very strict with naming and coding conventions will help, but it is still a burden, even more so if multiple developers are involved.
Reading through NAnt documentation before the latest build script refactoring task, we managed to find a better way to implement calling of such independent targets, by using nant
instead of call
:
<target name="wrapper">
<nant buildfile="${project::get-buildfile-path()}" target="hello" />
<nant buildfile="${project::get-buildfile-path()}" target="hello">
<properties>
<property name="hello.name" value="Europe" />
</properties>
</nant>
<nant buildfile="${project::get-buildfile-path()}" target="hello" />
<nant buildfile="${project::get-buildfile-path()}" target="hello">
<properties>
<property name="hello.name" value="Slovenia" />
</properties>
</nant>
<nant buildfile="${project::get-buildfile-path()}" target="hello" />
</target>
<target name="hello">
<property name="hello.name" value="World"
unless="${property::exists('hello.name')}" />
<echo message="Hello, ${hello.name}." />
</target>
The above target still calls hello
from within the same file, but it launches it in a separate context, isolating its properties from global ones. if we look at the output, the default values are now used every time they are not explicitly passed to the called target:
Hello, World.
Hello, Europe.
Hello, World.
Hello, Slovenia.
Hello, World.
Of course the called target can now even be moved to a separate file and then easily reused from multiple scripts, making it even more modular. The only real downside of this approach is its verbosity. Although each nant task is effectively just a method call, it requires much more typing than we are used from any real programming language.