MSBuild in a nutshell

Objetivo

Recentemente, quando estava escrevendo os artigos sobre integração, estava novamente me matando para organizar console applications, tratar uma série de parâmetros quando lembrei do MSBuild.

Já trabalhei com ele um bom tempo, mas nunca tirei um tempinho pra escrever sobre ele. Agora aproveitei que ia ter que explicar o meu código do outro post, e resolvi escrever.

Pra que serve o MSBuild?

O MSBuild é uma ferramenta de build (genial!). Serve para escrever scripts de build de aplicativos, encadeando uma série de rotinas referentes a apagar arquivos, realizar build, executar testes unitários, empacotar, gerar instalador, carimbar versão e qualquer outra coisa que se queira fazer.

Eu penso sempre em ferramentas de build como um belo canivete suíço para automatizar um monte coisas repetitivas que fazemos no dia-a-dia de desenvolvimento. Gosto muito da prática de escrever scripts de build, pois sempre pensamos: “já tenho uma solution que combina todos os meus projetos, não preciso disso”, mas na prática nem sempre as aplicações estão escritas numa única tecnologia, ou ainda, nem sempre estão em uma única solution, ou não depende somente de compilação (aqui está o grande erro).

Quando utilizamos uma ferramenta de build e criamos um script para realizar tudo aquilo que garante a qualidade da nossa aplicação associada a um servidor de integração contínua (Ex.: Cruise Control.NET), começamos a descobrir uma série de coisas sobre nosso código e nosso time.

Todas as vezes que implementei essas práticas pelas empresas que passei, descobri que nem todo mundo tinha o mesmo cuidado ao realizar a subida dos fontes para o controle de versão, ou ainda, nem todos executavam os testes unitários antes de subir os fontes. A dupla dinâmica ferramenta de build + servidor de integração contínua ajudam muito a criar essa cultura na equipe.

Nesse post, não vou falar de integração contínua, apenas uma rápida introdução sobre MSBuild que ajuda a dar um start nessa prática.

MSBuild na prática

O MSBuild é uma console application que vem distribuída junto da framework. Sozinha não faz absolutamente nada. No framework 4.0, ela fica no caminho: C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe

A referência completa dos parâmetros de linha de comando do MSBuild, você encontra no MSDN (MSBuild Command Line Reference). Não vou passar um a um aqui. Apenas os principais:

msbuild [ProjectFile] /t:[Targets] /l:[Logger] /p:[Name=Value] 
  • Project File é o xml na estrutura do MSBuild que possui todas a instrução de build
  • Targets é a lista de “Targets” a serem executados. Um target é um conjunto de atividades (tasks). Mais abaixo veremos como definí-los.
  • Logger é a classe utilizada para tratar o log do MSBuild. Veremos algumas opções também.
  • O /p permite a passagem de propriedades, ou seja, pares de nome e valor.

Para demonstrar a estrutura de um arquivo do MSBuild, vamos criar um arquivo MSBuildTests.build. Você pode usar o seu editor de textos favorito para editar o arquivo, geralmente pintando com sintaxe xml para facilitar a vida. Vamos criar com a seguinte estrutura:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">	
	<Target Name="SaySomething">
		<Message Text="Hello world!" />
	</Target>
</Project>

O xml acima exemplifica a estrutura padrão do MSBuild. A tag “Target” define um target com uma única task Message que indica um texto a ser devolvido no log. Para executarmos isso, vamos criar no mesmo diretório um arquivo runmsbuild.bat, com o seguinte conteúdo:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe MSBuildTests.build /t:SaySomething
pause

O batch acima executa o msbuild, referenciando o projeto MSBuildTests.build e o /t: indica que o target a ser executado é o “SaySomething”. Executando o batch, você deve receber o resultado:

C:\Exercicios\MSBuild\build&gt;C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe MSBuildTests.build /t:SaySomething
Microsoft (R) Build Engine Version 4.0.30319.1
[Microsoft .NET Framework, Version 4.0.30319.261]
Copyright (C) Microsoft Corporation 2007. All rights reserved.

Build started 22/04/2012 19:08:21.
Project "C:\Exercicios\MSBuild\build\MSBuildTests.build" on node 1 (SaySomethin
g target(s)).
SaySomething:
  Hello world!
Done Building Project "C:\Exercicios\MSBuild\build\MSBuildTests.build" (SaySome
thing target(s)).


Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.03

C:\Exercicios\MSBuild\build&gt;pause
Pressione qualquer tecla para continuar. . .

Isso significa que tudo funcionou. Agora basta montar sua seqüencia de ações para montar o seu build.

Propriedades

Para simplificar a vida em relação a parâmetros, o MSBuild nos permite definir propriedades. Vejamos o exemplo:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">	
	<PropertyGroup>
		<TextToSay>This is a property</TextToSay>	
	</PropertyGroup>

	<Target Name="SaySomething">
		<Message Text="$(TextToSay)" />
	</Target>
</Project>

Ao executarmos, veremos que $(TextToSay) é substituído pelo valor da propriedade definido no começo do arquivo. Também é possível sobrescrever o valor, passando parâmetros pelo arquivo batch:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe MSBuildTests.build /t:SaySomething /p:TextToSay="Novo valor"
pause

Realizando o build de uma solução

Para fazermos algo mais útil com o MSBuild, podemos utilizá-lo num processo padrão de build de uma solution do visual studio. Segue o exemplo:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">	
	<PropertyGroup>
		<TextToSay>This is a property</TextToSay>	
	</PropertyGroup>
	
	<ItemGroup>
		<Binaries Include="$(MSBuildProjectDirectory)\..\MSBuildTests\**\*.dll" />
	</ItemGroup>

	<Target Name="SaySomething">
		<Message Text="$(TextToSay)" />
	</Target>
	
	<Target Name="Build">
		<Delete Files="@(Binaries)" />
		<MSBuild Projects="$(MSBuildProjectDirectory)\..\MSBuildTests\MSBuildTests.sln" />
	</Target>
</Project>

Aqui tem vários detalhes interessantes. O primeiro é que qualquer solution do Visual Studio é automaticamente reconhecida pelo MSBuild, ou seja, simplesmente executando a Task “MSBuild” numa solution, ela compila. É possível também passar parâmetros, definindo a configuração (Release, Debug) ou outras coisas que a solution permita.

O segundo detalhe interessante é que existem algumas propriedades “padrões” do MSBuild como a $(MSBuildProjectDirectory). No exemplo acima, para encontrar a solution eu uso um path relativo para ela, de forma que independente de onde estejam os arquivos do meu projeto, eles serão encontrados pelo build.

O terceiro detalhe é que eu usei o nome “Build” para a task, que é o mesmo que está no “DefaultTargets” na tag Project. Apenas removendo o /t:SaySomething do arquivo .bat, ela é automaticamente executada, porque foi especificada como default.

Agora um detalhe que sempre gera confusão é o ItemGroup. Neste exemplo, criei um item chamado “Binaries” e a máscara fornecida acima pega qualquer arquivo com extensão .dll em qualquer subdiretório abaixo de MSBuildTests. A idéia é apagar todos os binários antes de compilar (usei apenas como exemplo). Ao chamar a task Delete, todo o conjunto de arquivos encontrados que bate com aquela máscara será excluído.

Outro detalhe importante sobre o “ItemGroup” é que eles são avaliados no momento que começa a execução do projeto. Se você gera arquivos durante o build e quer considerá-los, use a task “CreateItem” para montar os grupos de arquivos.

Criando tasks customizadas

Existe uma série de tasks pré-definidas e sua referência completa pode ser encontrada no link: MSBuild Task Reference. Mesmo assim, é possível construir suas próprias para que você possa fazer suas maluquices.

Para construir uma task personalizada, basta criar uma class library (no meu exemplo, MSBuildTests.Tasks) e criar uma nova classe. É importante criar uma referência para os assemblies Microsoft.Build.Utilities.v4.0 e Microsoft.Build.Framework.

Segue o exemplo de código de uma task:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.IO;

namespace MSBuildTests.Tasks
{
    public class GenerateDumbFiles : Task
    {
        [Required]
        public string Directory
        {
            get;
            set;
        }

        [Required]
        public string Prefix
        {
            get;
            set;
        }

        [Required]
        public int Count
        {
            get;
            set;
        }

        public override bool Execute()
        {
            for (int i = 0; i < Count; i++)
            {
                FileStream fs = new FileStream(Directory + "\\" + Prefix + (i+1).ToString() + ".txt", FileMode.Create);
                using (fs)
                {
                    byte[] content = Encoding.UTF8.GetBytes("I'm a dumb file");
                    fs.Write(content, 0, content.Length);
                }
            }
            return true;
        }
    }
}

No exemplo acima, dado um prefixo e uma quantidade de arquivos, a task gera arquivos idiotas. Segue exemplo de como usá-la:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">	
	<PropertyGroup>
		<TextToSay>This is a property</TextToSay>	
	</PropertyGroup>
	
	<ItemGroup>
		<Binaries Include="$(MSBuildProjectDirectory)\..\MSBuildTests\**\*.dll" />
	</ItemGroup>
	
	<UsingTask TaskName="GenerateDumbFiles" AssemblyFile="$(MSBuildProjectDirectory)\..\MSBuildTests\MSBuildTests.Tasks\bin\Debug\MSBuildTests.Tasks.dll" />

	<Target Name="SaySomething">
		<Message Text="$(TextToSay)" />
	</Target>
	
	<Target Name="Build">
		<Delete Files="@(Binaries)" />
		<MSBuild Projects="$(MSBuildProjectDirectory)\..\MSBuildTests\MSBuildTests.sln" />
		<MakeDir Directories="$(MSBuildProjectDirectory)\..\dumbfiles" />
		<GenerateDumbFiles Directory="$(MSBuildProjectDirectory)\..\dumbfiles" Prefix="DumbFile" Count="100" />
	</Target>
</Project>

O que inserimos de novidade aqui foi a criação do diretório dumbfiles e a tag UsingTask. Esta tag é quem diz para o MSBuild o nome da Task que estamos procurando e qual assembly ele deve carregar. Notem que o assembly é compilado e carregado em tempo de build. A task que está sendo usada dentro do build está sendo compilada em tempo de build.

A tag GenerateDumbFiles simplesmente executa o código da task que criamos, respeitando os parâmetros “Required”. Se não especificarmos nenhum, dá erro na execução da task, assim como se o assembly não for localizado, ou internamente dentro do assembly o nome da classe não bater com o TaskName especificado.

Logger

O MSBuild permite também que você “espete” classes de log específicas. É comum que com o tempo os builds fiquem bastante grandes e pra entender o que aconteceu dá um certo trabalho. Debuggar com as cores é bem mais legal e o logger default não suporta.

Aí você procura na internet e acha um cara legal que fez um logger em xml, como este: http://geekswithblogs.net/kobush/archive/2006/01/14/xmllogger.aspx. Você baixa o fonte dele, compila e pega a classe de logger.

No batch de execução do MSBuild, você coloca o seguinte parâmetro:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe MSBuildTests.build /p:TextToSay=”Novo valor” /l:XmlLogger,Kobush.Build.dll;msbuild-output.xml
pause

Isso indica que ele carregará a classe do Kobush de log, e o output será no arquivo msbuild-output.xml. Se executar, você verá o resultado. Ainda assim não estando satisfeito, você pode brincar mais ainda com o MSBuild, realizando uma transformação de xml para um formato mais legal.

Agora adicionamos também no nosso xml, um target:

<Target Name="TransformLog">
		<XslTransformation XmlInputPaths="$(MSBuildProjectDirectory)\msbuild-output.xml" XslInputPath="$(MSBuildProjectDirectory)\msbuild.xsl" OutputPaths="$(MSBuildProjectDirectory)\log.html" />
	</Target>

E alteramos nosso .bat para também executar este target:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe MSBuildTests.build /p:TextToSay="Novo valor" /l:XmlLogger,Kobush.Build.dll;msbuild-output.xml
C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe MSBuildTests.build /t:TransformLog
pause

O resultado final é um arquivo “log.html” com uma transformação do log num formato “bonitinho” para debug.

É claro que é possível também implementar seus próprios loggers. Mas não pretendo abordar isso neste post.

Código fonte

O código fonte está disponível no git hub: https://github.com/ericlemes/MSBuildTests

Conclusão

O MSBuild é uma poderosa ferramenta para nos ajudar a orquestrar diversas tarefas de automação de build. Toda a complicação de encontrar caminhos relativos, administrar parâmetros de execuções de pequenos aplicativos ou console applications é praticamente eliminado usando o MSBuild.

A própria prática de ter um “roteiro” para empacotar uma aplicação já adiciona muito valor ao processo de desenvolvimento, pois permite que cada rotina, biblioteca ou atividades realizadas para construir a aplicação esteja documentada e seja mantida junto com o build.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s