MSBuild: Benutzerdefinierte Build-Aufgaben entwickeln
Schon vor langer Zeit habe ich mich erfolgreich vom F5-Build befreit :-) In den letzten beiden
Wochen habe ich wieder verstärkt an der Anpassung verschiedener Build-Prozesse gewerkelt und auch
meine lieben Kollegen in Sachen MSBuild gepimpt. Was mich heute dazu veranlasst hat, einige
Informationen zum Thema benutzerdefinierte Build-Tasks aufzuschreiben und hier zu veröffentlichen...
Alles was ich benötige, um eigene Tasks für MSBuild zu entwickeln, ist Bestandteil des .NET Framework.
Wie neue Tasks mit Hilfe von Visual Studio entwickelt werden können, habe ich hier kurz und knapp
aufgeschrieben (ich setze allerdings voraus, das Sie bereits wissen, was MSBuild ist und wie es
benutzt wird).
Man benötigt eine leeres Assembly-Projekt (Klassenbibliothek), in dem folgende
Verweise gesetzt werden:
- Microsoft.Build.Engine
- Microsoft.Build.Framework
- Microsoft.Build.Tasks
- Microsoft.Build.Utilities.v3.5
Eine MSBuild-Aufgabe wird jeweils durch eine Klasse repräsentiert, die die
Microsoft.Build.Framework.ITask-Schnittstelle implementiert. Da diese Schnittstelle Member
definiert, die zur Unterstützung der MSBuild-Infrastruktur bestimmt sind und diese je Aufgabe
immer wieder neu zu implementieren wären, habe ich mir eine Task-Basisklasse gebastelt, die
ich für neue Aufgaben nutze. Diese sieht so aus...
public abstract class Task : Microsoft.Build.Framework.ITask
{
protected Task()
{
}
public IBuildEngine BuildEngine
{
get;
set;
}
public abstract bool Execute();
public ITaskHost HostObject
{
get;
set;
}
}
Und dann kann es auch direkt losgehen... Als Beispiel habe ich mir eine GetFileInfo-Aufgabe
überlegt, die die Informationen, die mit Hilfe der System.IO.FileInfo-Klasse ermittelt werden
können, in MSBuild verfügbar zu machen.
public class GetFileInfo : Task
{
public GetFileInfoTask() : base() { }
[Required()]
public ITaskItem[] Files
{
get;
set;
}
[Output()]
public ITaskItem[] FileInfos
{
get;
set;
}
public override bool Execute()
{
...
return true;
}
}
Die GetFileInfo-Klasse erbt von Task und überschreibt die Execute-Methode, welche die
Aktionen der Aufgabe ausführt. Geben Sie den Wert true zurück, wenn die Aufgabe ordnungsgemäß
ausgeführt werden konnte, andernfalls false.
Die GetFileInfo-Klasse definiert außerdem die Eigenschaften Files und FileInfos, die die
Ein- und Ausgabe-Parameter der Aufgabe darstellen. Die Files-Eigenschaft ist mit einem
Required-Attribut gekennzeichnet – somit wird dieser Parameter zur Pflicht. Die
FileInfos-Eigenschaft stellt einen Ausgabeparameter dar; welche mit einem Output-Attribut
gekennzeichnet werden.
Für Task-Eigenschaften können nur Typen verwendet werden, die durch MSBuild unterstützt
werden – in den meisten Fällen werden Sie mit System.String oder
Microsoft.Build.Framework.ITaskItem bzw. mit Arrays dieser Typen auskommen, wobei einige
Besonderheiten zu beachten sind. Zunächst zeige ich jedoch, wie die GetFileInfo-Aufgabe
im Script verwendet werden kann.
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
DefaultTargets="Build">
<PropertyGroup>
<CustomTasksDir> ... </CustomTasksDir>
</PropertyGroup>
<UsingTask AssemblyFile="$(CustomTasksDir)CustomTasks.dll"
TaskName="CustomTasks.GetFileInfo" />
<ItemGroup>
<Files Include="C:\temp\*.*" />
</ItemGroup>
<Target Name="Build">
<CustomTasks.GetFileInfo Files="@(Files)">
<Output ItemName="FileInfos"
TaskParameter="FileInfos" />
</CustomTasks.GetFileInfo>
</Target>
</Project>
Mit Hilfe der UsingTask-Aufgabe kann die GetFileInfo-Aufgabe aus dem CustomTasks.dll-Assembly
geladen werden (dabei muss die Aufgabe über den TaskName-Parameter mit vollem Typ-Bezeichner
deklariert werden). Der Aufruf der GetFileInfo-Aufgabe erfolgt über das Build-Ziel, wobei
im Files-Parameter eine Auflistung von Dateien gesetzt wird.
Nun zurück zu den Task-Eigenschaften...
In meinem Beispiel habe ich die Files-Eigenschaft als ITaskItem-Array deklariert.
...
[Required()]
public ITaskItem[] Files
{
get;
set;
}
...
Das bewirkt, das jedes Element aus Files als TaskItem übergeben wird; so...
Alternativ kann aber auch System.String bzw. System.String-Array verwendet werden.
Ein Array von String macht hier allerdings keinen bedeutenden Unterschied...
Wird allerdings String verwendet, werden die Elemente als komma-separierte Liste
übergeben – was natürlich nicht so praktisch ist, da diese für die weitere Verarbeitung
erst wieder gesplittet werden müsste.
Der ITaskItem-Typ ist für alle Einsatzzwecke am besten geeignet. In den meisten Fällen
kann über die ItemSpec-Eigenschaft eine Zeichenfolgenentsprechung abgerufen werden; wenn
nicht, sollte die gewünschte Information in den Metadaten des jeweiligen Elements enthalten sein.
Als Ergebnis soll über die FileInfos-Eigenschaft eine Auflistung von Dateiinformationen
zurückgeliefert werden, die folgender Struktur entspricht:
<ItemGroup>
<FileInfos Include="Item">
<Exists />
<FullName />
<DirectoryFullName />
...
</FileInfos>
</ItemGroup>
Dazu kann die Microsoft.Build.Utilities.TaskItem-Klasse verwendet werden (oder man bastelt
eine eigene Implementierung mit Hilfe der ITaskItem-Schnittstelle). Die Implementierung der
Execute-Methode könnte so aussehen...
...
public override bool Execute()
{
List<TaskItem> taskItems = new List<TaskItem>();
foreach (ITaskItem nextItem in this.Files)
{
FileInfo fi = new FileInfo(nextItem.ItemSpec);
TaskItem fileItem = new TaskItem(fi.FullName);
fileItem.SetMetadata("Exists", fi.Exists.ToString());
fileItem.SetMetadata("FullName", fi.FullName);
fileItem.SetMetadata("DirectoryFullName", fi.Directory.FullName);
…
taskItems.Add(fileItem);
}
this.FileInfos = taskItems.ToArray();
return true;
}
...
Und das geht raus...
Im Build-Script können die Informationen dann wie folgt genutzt werden...
...
<Target Name="Build">
<CustomTasks.GetFileInfo Files="@(Files)">
<Output ItemName="FileInfos"
TaskParameter="FileInfos" />
</CustomTasks.GetFileInfo>
<Message Text="File: %(FileInfos.FullName), Size: %(FileInfos.Length)"
Condition="%(FileInfos.Exsist == 'true')" />
</Target>
...