Inleiding
Met de release van Visual Studio 2005 heeft Microsoft de scope verbreed van de Developer naar de overige rollen in het team. Zo is er nu ook ondersteuning voor de Tester, de Architect en de Projectleider. Deze verbreding is noodzakelijk om in grotere teams maatwerksystemen te kunnen bouwen. Deze verbreding is ook terug te vinden op andere vlakken, zoals het build mechanisme. Dit artikel gaat in op het nieuwe build systeem voor .NET 2.0 (MSBuild), geeft aan hoe Visual Studio daar zelf ook gebruik van maakt en hoe het aan te passen is.
Een belangrijk mechanisme voor het bouwen van een wat groter systeem is het concept van de automatische build, waarbij het gehele systeem vanuit sourcecode wordt opgebouwd tot een uitvoerbaar geheel, wat vervolgens uitgerold en getest kan worden.
Het is wenselijk om een automatische build regelmatig te draaien, om zo te controleren of na wijzigingen en toevoegingen de bestaande en nieuwe functionaliteit goed werkt.
Dit in tegenstelling tot het patroon wat in het verleden vaak toegepast werd, waarbij na ontwerp eerst enkele weken of soms maanden gebouwd werd en pas aan het eind van de periode alle delen werden geïntegreerd en getest.
Door vaker de code van een project te integreren (het hele systeem bouwen en testen) komen eventuele problemen vroeg aan het licht en kunnen ze in de kiem gesmoord worden
Visual Studio 2003 en de .NET 1.1 omgeving ondersteunen dit concept van een automatische build maar gedeeltelijk. Het is mogelijk om een automatische build te doen door devenv.exe via de commandline aan te roepen, maar dit is niet ideaal. Er spelen een paar belangrijke problemen:
- Visual Studio 2003 moet aanwezig zijn op de machine die de build uitvoert. Dit kan er voor zorgen dat er extra libraries op die machine aanwezig zijn waardoor het geheel ‘per ongeluk’ werkt. De wens is om de build machine zo schoon mogelijk te houden (en dus alleen de SDK en geen Visual Studio).
- Het toevoegen van extra stappen (bijvoorbeeld documentatie generatie) is alleen mogelijk voor of na de build van Visual Studio, je kunt er niet ‘tussen’ gaan zitten.
- Het vervangen of aanpassen van de build stappen van een Visual Studio project is niet mogelijk
Een alternatief is om, in plaats van devenv.exe aan te roepen, gebruik te maken van de Open Source tool NAnt. Deze biedt een XML structuur waarmee bijvoorbeeld een automatische build aangestuurd kan worden. NAnt biedt een aantal specifieke commando’s die dit goed mogelijk maken. Zo zijn er standaard taken voor het bouwen van een Visual Studio Solution en interactie met Visual Source Safe.
Het gebruik van NAnt heeft niet de eerder genoemde nadelen van devenv.exe. Maar een nadeel is wel, dat het een mechanisme is wat naast Visual Studio staat. Dit levert in een aantal gevallen extra onderhoud op.
Naast NAnt en devenv.exe is er sinds jaar en dag de optie om met NMake te werken, maar dit betekent nog meer handmatig onderhoud, omdat dit nog minder geïntegreerd is met Visual Studio. Daarnaast hebben de files geen XML structuur, waardoor ze lastiger geautomatiseerd te genereren en te lezen zijn door eigen tools.
Met .NET 2.0 en Visual Studio 2005 is er verandering gekomen in dit alles. De architectuur is nu namelijk zo dat er maar één build-engine is. Deze kan los gebruikt worden, dus zonder dat Visual Studio geïnstalleerd is. Daarnaast maakt Visual Studio zelf onder water gebruik van exact diezelfde engine (zie ook figuur 1).
Sterker nog, Visual Studio project files zijn tegenwoordig simpelweg MSBuild files.

Fig. 1: MSBuild architectuur
MSBuild
Om het build gedrag van een Visual Studio project aan te kunnen passen of uit te kunnen breiden is dus MSBuild kennis benodigd. Daarom eerst een korte introductie in MSBuild.
Een MSBuild file is een Project (zie figuur 2). In een project zijn er twee dingen benodigd:
- Meta-Informatie, bijvoorbeeld welke files moeten er gebuild worden, van welk type zijn ze, enzovoorts;
- Taken; kunnen worden uitgevoerd waarbij ze gebruik kunnen maken van de meta-informatie.

Fig. 2: MSBuild project structuur
De meta informatie kan gestructureerd zijn op twee manieren, namelijk met een PropertyGroup of met een ItemGroup.
PropertyGroup
Een PropertyGroup is niets anders als een ‘gebied’ waarbinnen losse variabelen gedeclareerd kunnen worden. De genoemde variabelen hebben geen echte samenhang. Een PropertyGroup kan echter wel een conditie hebben waaronder de variabelen in de group wel of niet gevuld zullen worden.
Een XML element binnen een PropertyGroup declareert een variabele. Zo hebben we in figuur 2 dus drie variabelen: “AppName”, “DebugSymbols” en “OutputAssembly”.
Wanneer de waarde van de variabele benodigd is, kan deze opgevraagd worden via $(variabelenaam).
In figuur 2 is als voorbeeld te zien dat op deze manier de OutputAssembly dus altijd gelijk is aan de AppName, met daarachter geplakt “.exe”.
ItemGroup
Een ItemGroup is bedoeld voor collecties van elementen, zoals in figuur 2 waar alle te compileren files in een ItemGroup zitten. Een collectie wordt, net als bij een variabele, samengesteld op basis van de elementnaam.
De verwerking van de elementen in een ItemGroup kan op verschillende manieren.
De collectie kan als één parameter aan een verwerkende taak meegegeven worden door middel van de volgende syntax: @(groupnaam).
Daarnaast is het soms ook nodig om niet een set van items aan een taak aan te bieden, maar om juist per item in de collectie een taak uit te voeren. In dat geval wordt de Itemgroup op de volgende manier gerefereerd: %(groupnaam).
Zie figuur 3 voor een voorbeeld van beide situaties.

Figuur 3: ItemGroup verwerking
Tasks & Targets
Meta informatie op zichzelf is natuurlijk niet zinvol. Je moet er wel wat mee doen. Daarvoor zijn zogenaamde targets beschikbaar. Een target is een sectie in een MSBuild file die uitgevoerd kan worden. Binnen die sectie zijn tasks opgenomen, die het daadwerkelijke werk doen. In figuur 2 is er b.v. een target met de naam “Build”, die als een van de tasks het aanroepen van de C# compiler (csc) uitvoert.
Voor een target geldt dat deze altijd sequentieel verwerkt wordt en dus worden tasks dan een voor een na elkaar uitgevoerd. Targets kunnen echter ook afhankelijkheden hebben naar andere Targets. In dat geval wordt eerst de Target geëvalueerd waar de afhankelijkheid naar toe ligt, en dit wordt vervolgens recursief toegepast. MSBuild zorgt er dus voor dat afhankelijke targets in de juiste volgorde worden uitgevoerd.
Op deze manier wordt het mogelijk om heel modulair een aantal basiszaken te regelen waar vervolgens meerdere targets gebruik van maken door een afhankelijkheid te leggen. Zo kan er bijvoorbeeld een Target “Clean” gedefinieerd worden die netjes alle gegenereerde files en directories leeg maakt, zodat er weer een verse build gemaakt kan worden. De “Build” Target is hier dan niet van afhankelijk, omdat die alleen gewijzigde zaken opnieuw dient te builden. Door deze twee te combineren ontstaat echter een nieuwe mogelijkheid, namelijk het target “Rebuild”, die zelf geen echte inhoud heeft, maar simpelweg twee afhankelijkheden heeft, naar “Clean” en vervolgens “Build”.
Een task is iets dat simpelweg uitgevoerd wordt. Een task wordt gedefinieerd door een class in een assembly, en daar wordt dus de link gemaakt met executeerbare code.
Door het UsingTask element te gebruiken in MSBuild wordt de task bekend gemaakt in de MSBuild omgeving.
Conditions
Op alle genoemde dingen in een MSBuild file kunnen condities losgelaten worden. Zie figuur 4 voor een voorbeeld hiervan. Hierbij worden er alleen files gekopieerd waarvan de extensie “.TXT” is.

Figuur 4: ItemGroup verwerking
Bij deze conditie wordt gebruik gemaakt van zogenaamde well-known metadata die beschikbaar is in MSBuild. Zie tabel 1 voor de rest van deze metadata.
| Item Metadata |
Description |
| %(FullPath) |
Contains the full path of the item. For example: C:\MyProject\Source\Program.cs |
| %(RootDir) |
Contains the root directory of the item. For example: C:\ |
| %(Filename) |
Contains the file name of the item, without the extension. For example: Program |
| %(Extension) |
Contains the file name extension of the item. For example: .cs |
| %(RelativeDir) |
Contains the directory path relative to the current working directory. For example: Source\ |
| %(Directory) |
Contains the directory of the item, without the root directory. For example: MyProject\Source\ |
| %(RecursiveDir) |
If the Include attribute contains the wildcard **, this metadata specifies the directory that replaced the wildcard to find the item. This example has no RecursiveDir metadata, but if the the following example was used to include this item, the item would contain a RecursiveDir value of MyProject\Source\.
|
| %(Identity) |
The item specified in the Include attribute.. For example: Source\Program.cs |
| %(ModifiedTime) |
Contains the timestamp from the last time the item was modified. For example: 2004-07-01 00:21:31.5073316 |
| %(CreatedTime) |
Contains the timestamp from when the item was created. For example: 2004-06-25 09:26:45.8237425 |
| %(AccessedTime) |
Contains the timestamp from the last time the time was accessed. For example: 2004-08-14 16:52:36.3168743 |
Tabel 1: Well-known metadata (uit MSDN)
Loggers
Bij een build systeem is het natuurlijk van groot belang om te weten wat er tijdens build wel en niet gelukt is. Logging is daarom noodzakelijk. MSBuild biedt standaard logging aan naar de console of naar een file. Op het moment dat logging op een andere manier moet plaatsvinden, is het ook mogelijk om een eigen Logger aan MSBuild te koppelen.
In Visual Studio Team System wordt er bijvoorbeeld een speciale logger aangeleverd om er zo voor te zorgen dat build informatie in de Team Foundation Server terecht komt en deze dus de status kan bewaken.
TARGET hiërarchie
Het patroon om een build te maken is redelijk standaard. Om dit op een generieke manier te kunnen hergebruiken en aan te passen wordt er gebruik gemaakt van “.TARGET” files. Dit zijn eigenlijk files met standaard MSBuild targets, die door middel van een Import command in een MSBuild file bekend gemaakt kunnen worden.
Zo is de Build target voor C# projecten kant en klaar ingevuld met alle stappen die nodig zijn om een C# project te bouwen. Deze kant en klare targets zijn opgenomen in het bestand “Microsoft.CSharp.targets”. Dit bestand maakt vervolgens weer gebruik van “Microsoft.Common.targets” waar alles gedefinieerd is wat taal onafhankelijk is. Het blijkt namelijk dat bijvoorbeeld het builden van een VB of C# project niet echt verschillend is, op het aanroepen van de compiler na. Beide typen projecten hebben dus een target file, die vervolgens weer van de common targets gebruik maken.
Deze standaard bestanden en de rest van de MSBuild engine, die overigens bij het .NET framework geleverd worden, zijn te vinden in de .NET Framework directory:
- %SYSTEMROOT%\Microsoft.NET\Framework\v2.0.50727
Uitvoeren van een build
Wanneer met behulp van eerdergenoemde zaken een geldig MSBuild file gemaakt is, dan kan deze uitgevoerd worden door deze aan MSBuild.exe aan te bieden:
MSBuild.exe /target:[targetnaam] [projectfilenaam]
Op de command-line kunnen properties in de projectfile eventueel nog aangepast worden door een extra command-line optie te specificeren, zoals:
/property:WarningLevel=2;OutputDir=bin\Debug
Build in Visual Studio 2005
Zoals gezegd sluit Visual Studio 2005 naadloos aan op dit mechanisme doordat een projectfile simpelweg een MSBuild-file is. Om dit te verifiëren kan een projectfile (bijvoorbeeld van een C# project) in Notepad geopend worden, en dit blijkt dan aan bovenstaande XML structuur te voldoen.
Er is echter ook een manier om de MSBuild file in Visual Studio zelf te bekijken en eventueel aan te passen:
- Klik rechts op het project en kies “Unload project”
- Klik wederom rechts op het project en kies “Edit
”
Het aanpassen van de build kan op een aantal niveaus gebeuren. Zo zijn er bijvoorbeeld standaard de targets “BeforeBuild” en “AfterBuild” beschikbaar om voor of na de build extra stappen uit te voeren.
Je bent nu namelijk niet meer beperkt tot het aanroepen van een commandline programma, maar kunt eigen tasks gaan aanroepen
Alleen dit geeft al meer flexibiliteit dan het uitvoeren van pre- en post-build events zoals dit al mogelijk was in Visual Studio. Je bent nu namelijk niet meer beperkt tot het aanroepen van een commandline programma, maar kunt eigen tasks gaan aanroepen.
Een voorbeeld hiervan is om een afterbuild target te maken waar vervolgens een NDoc-task gebruikt wordt om de bijbehorende documentatie te genereren.
De volledige Build cyclus bevat flink wat stappen, waarvan er voor een aantal zogenaamde “Before/After” targets gedefinieerd zijn, waar je zonder meer een eigen invulling aan kunt geven. Deze zijn genoemd in tabel 2.
| Buildtargets |
Moment |
| Before/AfterBuild |
Voor/na de gehele build cyclus (Build command in VS) |
| Before/AfterRebuild |
Voor/na de gehele rebuild cyclus (dus alleen bij rebuild) |
| Before/AfterResolveReferences |
Voor/na het opzoeken van references naar andere projecten en assemblies |
| Before/AfterResGen |
Voor/na het genereren van resources |
| Before/AfterCompile |
Voor/na de compilatie stap |
| Before/AfterClean |
Voor/na het opschonen (Clean command in VS) |
| Before/AfterPublish |
Voor/na het publiceren (Publish command in VS) |
Tabel 2: Voorbeelden van uitbreidbare targets
Door het projectfile aan te passen kunnen er dus custom stappen worden uitgevoerd. Het is echter nogal onhandig om dit steeds opnieuw te moeten doen voor elk project. Omdat C# projectfiles de Microsoft.CSharp.targets importeren, kan dat file aangepast worden om de extra stappen voor alle C# projecten te regelen. Meestal is dat echter te veel van het goede. Je moet immers ook nog gewoon ‘ouderwetse’ C# projecten kunnen maken.
De juiste methode is om voor je eigen uitbreidingen en aanpassingen een eigen targets file te maken. Die file importeert de Microsoft.CSharp.targets. Ten slotte laat je het C# project niet de Microsoft.CSharp.targets, maar je eigen targets file importeren.
Op deze manier heb je dan een eigen subtype gemaakt van een C# project. Je moet echter nog wel steeds één regel (namelijk de Import) in elk nieuw C# project aanpassen.
Eigen Projecttype
Ook dit probleem is op te lossen, en wel door een zogenaamd project template te maken. Je creëert dan eigenlijk een nieuwproject type in Visual Studio. Dit was ook in Visual Studio 2003 al mogelijk, maar toen nog redelijk bewerkelijk. In Visual Studio 2005 is dit gelukkig een stuk eenvoudiger geworden.
Als eerste dient het export template commando beschikbaar gemaakt te worden. Dit staat namelijk niet altijd standaard in het menu. Daarvoor klik je rechts op het menu en kiest vervolgens voor “Customize”. De Customize “Dialog” van figuur 5 komt vervolgens te voorschijn. Kies daar voor het file-menu en zoek het Export Template commando. Dit commando kan vervolgens ergens in de Visual Studio menu structuur gesleept worden, bijvoorbeeld onder het file menu.

Fig. 5: Customize dialog

Fig. 6: Export Template menu
Door het juiste project te selecteren en vervolgens het net toegevoegde “Export Template” te selecteren komt er een wizard te voorschijn: zie figuur 7a & 7b.

Fig. 7a: Stap 1 van de Export Template Wizard

Fig. 7b: Stap 2 van de Export Template Wizard
Hierbij kan aangegeven worden hoe het nieuwe projecttype moet heten, er kan eventueel een icoontje gekoppeld worden, etc.
Na het runnen van de Wizard is er een zip-file geproduceerd met het nieuwe projecttype. Om dit projecttype beschikbaar te maken dient het gekopieerd worden naar een sub-directory onder de volgende locatie: Program Files\Microsoft Visual Studio 8\Common7\IDE\ProjectTemplates,
Omdat in dit geval het een C# subtype betreft, is de zip nog wat dieper onder dit pad geplaatst, onder de subdirectory “CSharp” en daaronder de directory “MyProjects”.
De Zip file moet nu nog wel geïnstalleerd worden. Dit gebeurt door devenv.exe uit te voeren met als commandline optie “/setup”.
Na het opnieuw starten van Visual Studio is dan het nieuwe projec type beschikbaar, zoals aangegeven in figuur 8.

Fig. 8: Het nieuwe projecttype
Conclusie
De nieuwe Visual Studio en MSBuild omgeving biedt veel mogelijkheden om build-acties aan te passen en uit te breiden. Verder is al deze functionaliteit ook netjes geïntegreerd in de IDE.
Voor veel gebruikte ‘subtypes’ is het mogelijk om nieuwe project types te fabriceren, wat productiviteit en kwaliteit ten goede komt.