Automatizar la compilación con MSBuild

Cuando en mi anterior post explicaba las ventajas de usar un servidor de integración continua, decía que el primer paso para mejorar el proceso de integración es automatizar el proceso de compilación. En este post voy a explicar con un ejemplo sencillo cómo podemos automatizar este proceso usando MSBuild.

Para este ejemplo vamos a tener una solución de Visual Studio con dos proyectos: nuestra aplicación y un proyecto de tests, y nuestro script se va a encargar de realizar los siguientes pasos:

  1. Limpiar los resultados de ejecuciones anteriores.
  2. Compilar la solución.
  3. Ejecutar los tests.
  4. Generar un paquete NuGet con el resultado.

El objetivo será que alguien que se baje el proyecto desde el control de código fuente pueda realizar todos estos pasos ejecutando un único comando, sin necesidad de instalar manualmente ningún componente adicional en la máquina (más allá el SDK de .NET). De esta forma conseguiremos que la compilación siempre se realice en un entorno controlado, con versiones adecuadas de cada dependencia y sin posibilidad de introducir errores en el proceso por saltarnos algún paso manual.

Estructura de Carpetas

Cuando diseñamos un script de estas características, es importante definir una estructura de carpetas razonable. Esto, además de ayudarnos a crear el script, nos permitirá adaptarlo fácilmente a nuevos proyectos siempre que mantengamos la misma estructura de carpetas.

La estructura de carpetas que vamos a utilizar es la siguiente:

folders

La carpeta src será la carpeta donde se encontrará la solución de Visual Studio y los paquetes NuGet utilizados. Toda la gestión de paquetes NuGet la haremos desde Visual Studio, donde además de instalarlos deberemos hablitar la opción de Restaurar Paquetes NuGet (Enable NuGet Package Restore). Podríamos hacerlo desde el propio script de compilación usando NuGet por línea de comandos, pero resulta más cómodo hacerlo desde Visual Studio.

En la carpeta build generemos los archivos compilados (.exes y .dlls) y la usaremos como carpeta de trabajo durante la compilación.

La carpeta results contendrá en resultado final del proceso. En nuestro caso será un paquete NuGet y un fichero xml con los resultados de la ejecución de los tests.

En la carpeta raíz del proyecto tendremos el script de compilación propiamente dicho (sample.build) y un fichero build.cmd para lanzar el proceso cómodamente.

MSBuild: Targets y Tasks

MSBuild utiliza archivos XML para configurar el proceso de compilación. Estos ficheros se organizan alrededor del concepto de Targets. Un Target representa un proceso que se puede invocar de forma independiente y que, a su vez, puede depender de otros procesos.

En nuestro ejemplo, cada una de las fases de la compilación (limpiar, compilar, empaquetar, etc.) será un Target. Además crearemos un Target que dependa de todos los demás de manera que al invocarlo se ejecute el proceso completo.

La estructura del fichero incluyendo sólo los Targets es la siguiente:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Default">
  
  <Target Name="Default" DependsOnTargets="
          Clean;
          Build;
          RunTests;
          CreatePackage"/>
    
  <Target Name="Clean">...</Target>
  <Target Name="Build">...</Target>
  <Target Name="RunTests">...</Target>
  <Target Name="CreatePackage">...</Target>

</Project>

Para invocar un Target concreto por línea de comandos, debemos usar el parámetro /t. Si no indicamos nada, se ejecutará el Target por defecto (en este caso Default). Esto nos permite probar el script paso a paso:

msbuild.exe sample.build ← Esto ejecuta el target por defecto
msbuild.exe sample.bulid /t:RunTests ← Esto sólo ejecutaría el target RunTests

Para definir las acciones a realizar en cada Target se utilizan las Tasks. Las Tasks son operaciones que nos ofrece MSBuild para manejar archivos y directorios, compilar proyectos, ejecutar comandos externos, etc. Además de la que incluye MSBuild puedes crear tus propias Tasks o usar alguna de las librerías existentes, como MSBuild Extension Pack o MSBuild Community Tasks).

Al crear el script podemos definir propiedades y grupos de ficheros usando los elementos Property e ItemGroup. Las propiedades funcionan de manera similar a las variables, y podemos utilizarlas para almacenar la ruta hasta ficheros, parámetros de compilación, etc. Los grupos de ficheros sirven, como era de esperar, para indicar un conjunto de ficheros que luego podremos referenciar en las Tasks.

Aunque no voy a entrar en detalles para no alargar mucho el post, veamos cómo se implementa uno de los pasos: la ejecución de los tests:

<PropertyGroup>
  <BuildDir>$(MSBuildProjectDirectory)\build\</BuildDir>
  <ResultsDir>$(MSBuildProjectDirectory)\results\</ResultsDir>
  <NUnit>$(MSBuildProjectDirectory)\src\packages\NUnit.Runners.2.6.2\tools\nunit-console.exe</NUnit>
</PropertyGroup>  
  
<Target Name="RunTests">
  <Exec Command='"$(NUnit)" "$(BuildDir)\Tests.dll"' WorkingDirectory='$(ResultsDir)'/> 
</Target>

En el PropertyGroup declaramos una serie de propieades con la ruta hasta el ejecutable de NUnit-Console.exe que se encargará de lanzar los tests, la carpeta donde se encuentran los ficheros compilados y la carpeta donde se almacenan el resultado de los tests.

El Target RunTests utiliza la Task Exec para ejecutar NUnit-Console.exe pasándole como parámetros el assembly con los tests e indicando como carpeta de trabajo la carpeta de resultados. Como podéis ver, podemos usar string interpolation con las propiedades que hemos definido antes para construir los argumentos de la Task.

En el script de compilación completo podéis ver cómo se implementan el resto de Targets.

Resumen

Espero que este post haya servidor para desmitificar un poco el uso de MSBuild, podéis encontrar el código completo del proyecto en mi cuenta de github para jugar con él y ver cómo funciona.

Es importante tener una buena estructura de carpetas que podamos reutilizar de proyecto a proyecto, porque eso nos ayudará también a reutilizar el propio script de compilación. Lo normal es acabar con un script bastante genérico que luego se pueda adaptar a otros proyectos mediante la definición de propiedades y nuevos targets.

MSBuild es una herramienta muy potente y con él podemos automatizar muchas más cosas de las que se ven en este ejemplo. El mayor inconveniente que tiene (para mi gusto) es el uso de XML, que hace que resulte todo un poco más lioso, pero una vez que te acostumbras merece la pena.

5 comentarios en “Automatizar la compilación con MSBuild

  1. Hola Juanma. Estoy empezando con la integración continua, no he tenido valor para configurar CCNet… He optado finalmente por Jenkins. ¿Tienes alguna referencia?

    Tengo una duda respecto a la organización física en carpetas tanto del proyecto en si, como del resultado (result). ¿En un proyecto web utilizas la estructura que has utilizado en el ejemplo? ¿Dentro de esta carpeta sueles agrupar también por fecha de publicación/ versión? ¿Como gestionas el historial de compilaciones?

    Ahora la duda mas peliaguda… ¿Como gestionas las modificaciones de la base de datos que puede llevar cada nueva compilación?

    Quizás muchas preguntas… Gracias!

  2. Hola Miguel Ángel,

    Muy buenas preguntas.

    No tengo ninguna referencia sobre Jenkins porque sólo he usado CCNET (y para algún proyecto de pruebas travis-ci). Lo más importante es que tu script de compilación no dependa para nada del servidor de integración que uses. Ten en cuenta que deberías poder ejecutarlo en tu PC por línea de comandos, por lo que debería ser agnótico del servidor de integración (o al menos todo lo agnóstico que puedas). Así, tiene la ventaja añadida de que pasar de un servidor de integración a otro es fácil, sólo tienes que configurar que lance tu script de compilación y listo.

    Sobre la organización de carpetas, suelo usar una estructura similar independientemente del tipo de proyecto. La carpeta result contiene los resultado únicamente de la compilación en curso, no mantengo un histórico de binarios, porque para eso ya hago que el script de compilación genere una etiqueta en el control de versiones (Subversion en mi caso) cada vez que realiza una compilación completa, y siempre podría bajarme el código fuente correspondiente a una compilación y compilarlo por línea de comandos, consiguiendo exactamente el mismo resultado que se generó en su día.

    La parte de bases de datos es fácil.

    Todas mis aplicaciones tienen una herramienta para generar la base de datos inicial (y a veces cargarla con datos de ejemplo) a partir de mi modelo de objetos (si uso un ORM) o a partir de scripts sql. Esta herramienta suele ser una simple aplicación de consola y viene muy bien durante el desarrollo y durante el despliegue.

    Luego, cada versión tiene su migrador de base de datos que se encarga de actualizar el esquema desde la versión anterior, ya sea por código (al estilo de los migrations de EF) o con scripts sql. Estos migradores los uso básicamente en los despliegues en producción o si quiero recuperar un backup antiguo (por ejemplo para reproducir una incidencia) en desarrollo y probarlo con la última versión de la aplicación.

    Un saludo,

    Juanma.

  3. Cesar Verano dijo:

    Muy buenas preguntas si, podrias compartir el nombre de las herramientas que usas

    saludos ..

  4. Hola César,

    No sé si te refieres a mi o a Miguel Ángel, pero por si acaso, yo uso lo que he comentado en el post: MSBuild para el script de compilación y CCNET como servidor de integración de continua. El resto de cosas, depende del proyecto, pero generalmente suelo usar NUnit para escribir y ejecutar los tests, Wix para los paquetes de instalación y algunas herramientas de desarrollo propio para generar la documentación.

    Un saludo,

    Juanma

  5. Pingback: Ni grunt, ni gulp: solo npm | Koalite

Comentarios cerrados.