by Juanco Añez
Want lets you write your own customized tasks and make them available to
build scripts. In this short tutorial we'll write a basic "log" task for Want.
The complete source code for the example is included as <want>/src/tasks/LoggerTasks.pas
in the Want distribution.
To create a new Want task:
<want>/src/tasks/CustomTasks.pas
(Want's standard tasks go in StandardTasks.pas
). Here's an example:
unit LoggerTask; interface uses WantClasses; type TLoggerTask = class(TTask) public procedure Init; override; procedure Execute; override; end; implementation procedure TLoggerTask.Init; begin inherited Init; // Init attributes end; procedure TLoggerTask.Execute; begin Log('writing log info'); // do nothing more for now end; initialization RegisterTask(TLoggerTask); end.
And here's the uses clause in CustomTasks.pas
:
unit CustomTasks; interface uses { add the units for your customs task here } LoggerTask; implementation end.
WANT synthesizes an XML tag for your task from its class name using these rules:
The XML tag for the task in the above example would be 'logger'.
If you followed the above steps then, after recompiling Want, you should be able to use the the new task in build scripts:
<project name="myproject" default="main" > <target name="main" > <logger /> </target> </project>
To use an XML tag different from the automatically generated one, you can
override the TagName
class function in your task class, and provide
a different name:
TLoggerTask = class(TTask) public class function TagName :string; override; procedure Execute; override; end; //... class function TLoggerTask.TagName :string; begin Result := 'log'; end;
<project name="myproject" default="main" > <target name="main" > <log /> </target> </project>
WANT interprets the published properties of task classes as XML element attributes, and automatically manages them using Runtime Type Information (RTTI) (currently, only properties of types string, integer, boolean, enumeration, TPath and TStrings are supported). When defining properties that will map to XML attributes, keep in mind that:
We will add two properties/attributes to our logger: 'file', the file
to output to (required), and 'format', the format to use. Note that
'file' is a reserved keyword in Object Pascal, so we'll name the property
'_file'
instead and WANT will remove the leading underscore for us:
type TLogFormat = (brief, normal, detailed); TLoggerTask = class(TTask) protected FFile :string; FFormat :TLogFormat; public class function TagName :string; override; procedure Execute; override; published property _file :string read FFile write FFile; property format :TLogFormat read FFormat write FFormat; end; procedure TLoggerTask.Init; begin inherited Init; RequireAttribute('file'); end; <project name="myproject" default="main" > <target name="main" > <log file="${logs}/main.log" format="brief" /> </target> </project>
For convenience, WANT lets script writers specify attributes as nested elements. For example:
<project name="myproject" default="main" > <target name="main" > <log> <file path="${logs}/main.log" /> <format value="brief" /> </log> </target> </project>
Use the value attribute to specify the nested attribute's value. Use the path attribute when the value is a path.
The RequireAttribute
method is a handy way to verify that an
attribute was present in the build script.
WANT assigns the values of attributes to their corresponding properties as
the build script is parsed, and the Init
method is called after the
last attribute is processed.
The following macro expansions are performed before doing the assignment:
%{name}
is replaced by
the value of the environment variable "name
". ${name}
is replaced by
the value of the property "name
". Properties are defined using
the <property>
element anywhere in a build script:
<property name="aname" value="avalue" />
or passing the -Dname=value
command line option to want.exe.
={expre}
evaluates
expre
as a mathematical expression. @{path}
treats
path
as an want path (forward slashes) and converts it to a
system path. ?{inifile:section:key|default}
reads the value of the key
key, in section section
,
of the .ini file inifile
. If the key is not found, then the
optional default
is used for the value. These rules apply to properties:
<want>
task to override property
definitions in the build script. In other words, the build script may
say that:
<property name="debug" value="false" />
but a -Ddebug=true
on the command line will override
that.
<project>
level takes precedence over any definitions of the same
property in targets, tasks, and other elements. All that's left to do now is to define the task actions in terms of the
property/attribute values. Note that failure to complete the task should be
notified by calling the TTask.TaskFailure(Msg)
method:
procedure TLoggerTask.Execute; var LogFile :System.Text; begin Log(SysUtils.Format('writing log info to "%s"', [_file])); AboutToScratchPath(_file); System.Assign(LogFile, ToSystemPath(_file)); try if FileExists(ToSystemPath(_file)) then System.Append(LogFile) else System.Rewrite(LogFile); try Writeln(LogFile, DateTimeToStr(Now)); finally System.Close(LogFile); end; except TaskFailure('could not open log file'); end; end;
Note the following in the above code:
ToSystemPath(Path)
method.
TTask.AboutToScratchPath(Path)
method before changing any system file.
The AboutToScratchPath
method verifies the location of the passed
path and raises an exception if the file resides outside of the Want project's
base directory. TTask.Log(Msg)
method to provide some feedback to the user. Now we want to give our <log>
task a nested <info>
element that contains the text that the task will log. Scripts that use the
<log>
task would look like this:
<log file="${logs}/main.log" format="brief" > <info code="66" > The logger is working. </info> </log>
To define nested XML elements for a task:
TScriptElement
for each
kind of nested element. CreateXYZ
function in the task class, where "XYZ
" is the XML tag of the
nested element (in our example, the method will be called
TLoggerTask.CreateInfo
). The rules that Want uses for synthesizing an XML tag out of the names of
nested elements classes are like the ones used for tasks, except that the "Element
"
suffix is removed instead of the "Task
" one.
Instances of classes that descend from TScriptElement
and have a
valid owner passed to their constructor are automatically managed by Want, so
you don't need to delete them (call Free
on them). Valid owners are
instances of TTask
and its descendants, and instances of classes
that descend from TScriptElement
and have themselves a valid owner.
Here's the complete logger example:
unit LoggerTask; interface uses SysUtils, Classes, WantClasses; type TLogFormat = (brief, normal, detailed); TInfoElement = class(TScriptElement) protected FCode :Integer; FText :string; published property code :Integer read FCode write FCode; property text :string read FText write FText; end; TLoggerTask = class(TTask) protected FFile :string; FFormat :TLogFormat; FInfos :TList; public constructor Create(owner :TScriptElement); destructor Destroy; override; class function TagName :string; override; procedure Execute; override; published function CreateInfo :TInfoElement; property _file :string read FFile write FFile; property format :TLogFormat read FFormat write FFormat; end; implementation class function TLoggerTask.TagName :string; begin Result := 'log'; end; constructor TLoggerTask.Create(owner :TScriptElement); begin inherited Create(Owner); FInfos := TList.Create; end; destructor TLoggerTask.Destroy; begin // no need to free the TInfoElements themselves FInfos.Free; inherited Destroy; end; function TLoggerTask.CreateInfo :TInfoElement; begin Result := TInfoElement.Create(Self); FInfos.Add(Result); end; procedure TLoggerTask.Init; begin inherited Init; RequireAttribute('file'); end; procedure TLoggerTask.Execute; var LogFile: System.Text; i: Integer; begin Log(SysUtils.Format('writing log info to "%s"', [_file])); AboutToScratchPath(_file); System.Assign(LogFile, ToSystemPath(_file)); try if FileExists(ToSystemPath(_file)) then System.Append(LogFile) else System.Rewrite(LogFile); try for i := 0 to FInfos.Count-1 do begin with TInfoElement(FInfos[i]) do Writeln( LogFile, SysUtils.Format( '%20s %12s %s', [ FormatDateTime('yyyy/mm/dd hh:nn:ss', Now), '['+code+']', text ])); end; finally System.Close(LogFile); end; except TaskFailure('error writing log'); end; end; initialization RegisterTask(TLoggerTask); end. <project name="myproject" default="main" > <target name="main" > <log file="${logs}/main.log" format="brief" > <info code="66" > The logger is working. </info> </log> </target> </project>
In the above code, note the follwing:
CreateXYZ
methods, apply recursively to nested elements (nested
elements can have attributes and other nested elements). text
property of the corresponding TInfoElement
instance.
CreateXYZ
method assign the created element to an instance variable,
and raise an exception if the element has already been created. WANT uses an Element Registry to know which XML elements are available and how they can be nested. Instead of writing CreateElement methods for nested elements, you can use the registry to inform WANT of the intended element use. For example, the TDelphiCompile task has several sub-elements which are registered using the registry:
RegisterTasks( [TDelphiCompileTask, TResourceCompileTask]); RegisterElements(TDelphiCompileTask, [ TDefineElement , TUsePackageElement, TWarningElement ]);
The Element Registry allows developers to extend the syntax of WANT XML scripts without having to change any of the core WANT classes.
~o~
Copyright © 2001,2003 Juancarlo Añez, Caracas, Venezuela. All rights reserved.