WANT Task Writers Guide

by Juanco Añez

Basic Steps

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.

Go!

To create a new Want task:

  1. Create a new unit, and add WantClasses to the uses clause.
  2. Define a new class that descends from WantClasses.TTask.
  3. Override the Init procedure, and  verify that attributes have valid values after the task has been parsed.
  4. Override the Execute procedure to define the task's actions.
  5. Register the the class by calling WantClasses.RegisterTasks.
  6. Include the new unit in the uses clause of <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.

Choosing the Task's XML tag

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>

Handling XML attributes

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.

Making Attributes Required

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:

  1. Any appearance of a macro of the form %{name} is replaced by the value of the environment variable "name".
  2. Any appearance of a macro of the form ${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.

  3. Any appearance of a macro of the form ={expre} evaluates expre as a mathematical expression.
  4. Any appearance of a macro of the form  @{path} treats path as an want path (forward slashes) and converts it to a system path.
  5. Any appearance of a macro of the form ?{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:

Defining the Task's action

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:

Nested Task Elements

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:

  1. Define a class that descends from TScriptElement for each kind of nested element.
  2. For each kind of nested element, Define a published 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:

The Element Registry

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~