domingo 21 de diciembre de 2008

Automatizando el proceso de build de tu proyecto (1/n)

Al fin he terminado un pequeño proyecto personal que he estado realizando en mis ratos libres, que es la definición de un estándar personal para la organización y generación de mis proyectos en .NET. Suena muy rimbombante, pero no significa otra cosa que definir una estructura de directorios para organizar los diferentes ficheros que componen un proyecto (resources, ficheros de código), así como automatizar el proceso de build de un proyecto y los resultados generados (compilar, ejecutar tests y generar informes sobre el estado de la generación, etc).

Herramientas para automatización del proceso de build

En mi caso, el proceso de build de un proyecto incluye:

  • Compilar todos los módulos que componen el proyecto a partir de la última versión del código fuente(obteniendo dicho código del servidor de control de código)
  • Ejecutar los test-unitarios
  • Generar informes, en mi caso sólo del resultado de la ejecución previa de los test unitarios
  • Generar la documentación
  • Preparar y empaquetar todo lo generado para distribuir la última versión de la build

Al ser un proceso altamente repetitivo -y por tanto propenso a errores- es un gran candidato a una automatización. Para ello existen varias herramientas, siendo las más conocidas en el caso de .NET:

  • MSBuild propiedad de y utilizado por Microsoft (de hecho los ficheros *.*proj de Visual Studio son ficheros de definición de MSBuild).
  • NAnt, un port de la herramienta Ant que se creo para projectos Java.

He decidido implementar la automatización utilizando NAnt, simplemente por la posibilidad de aprovechar el conocimiento de cara a necesitar hacer algo con Ant para móviles, y porque me parecía subjetivamente más maduro: Microsoft lanzó MSBuild al público de forma gratuita e indepente de Visual Studio hace relativamente poco, y NAnt comenzó su andadura allá por el 2001. Además éste último es open source.

Ambas herramientas se basan en conceptos similares:

  • targets: Representa un resultado que queremos obtener en el proyecto. Típicos ejemplos de targets pueden ser 'clean', que elimina los ficheros binarios y temporales para dejar el código fuente preparado para compilar de nuevo, un target 'compile' para compilar el código y regenerar todo el proyecto, etc.
  • tasks: Las tareas son la base con las que realizamos las operaciones para llegar a un target. Están predefinidas, aunque podemos agregar tareas externas (NantContribes un proyecto para agregar tareas útiles no incluidas en el proyecto NAnt), así como crear tareas personalizadas. Típicas tareas pueden ser operaciones sobre los ficheros (mover, copiar, borrar o generar nuevos ficheros o directorios), llamar al compilador con una lista de ficheros para que genere un ejecutable o una librería, etc.
  • properties: Las propiedades nos permiten definir pares clave-valor dentro de la configuración. Se puede pensar en ellas como variables. Por ejemplo la propiedad project.sourcedirectory, podría representar el path al directorio donde se encuentran los ficheros de código del proyecto

Los ficheros de configuración de estas herramientas se construyen como ficheros XML con extensión .build, donde definimos los targets del proyecto y las tareas que lo componen, definimos las propiedades, etc.

ejemplo de fichero básico de build para NAnt

<project name="example nant-build">
<property name="mySolutionPath" value="C:\dev\testproject\solution.sln" />
<property name="myReferencesDir" value="C:\dev\testproject\references" />

<target name="compile">
<solution
configuration="Release"
solutionfile="${mySolutionPath}">
<projects>
<include name="${mySolutionPath}\other.csproj" />
</projects>
<excludeprojects>
<include name="${mySolutionPath}\toexclude.csproj" />
</excludeprojects>
<referenceprojects>
<include name="${myReferencesDir}\*.dll" />
</referenceprojects>

<echo message="Solution generated sucessfully!" />
</solution>
</target>
</project>

El anterior fichero de configuración define dos propiedades: 'mySolutionPath y myReferencesDir', el valor de las cuales es el path al fichero de solución Visual Studio del proyecto, y el path a un directorio de referencias necesarias para el proyecto respectivamente. Estas propiedades se encuentran bajo la etiqueta 'proyect' así que son visibles para todo target definido dentro de este proyecto. Las propiedades se referenciarán más tarde en el fichero de configuración encerrándolas entre ${ <nombre_propiedad> }


A continuación se define un target llamado 'compile', el cual consta de una sóla tarea: 'solution'. Esta tarea simplemente compilará el proyecto usando un fichero de solución (.sln) de visual studio especificado, por lo que equivale a ejecutar el comando Build (Generar) en dicho IDE. Por supuesto podemos tener tanto control como queramos a la hora de compilar el código, y podemos si así fuera necesario llamar directamente al compilador de C# con los argumentos apropiados y una lista de ficheros concretos, lo que puede ser muy útil en algunos casos (ahora mismo me viene a la cabeza que sería muy útil para generar http://en.wikipedia.org/wiki/.NET_assembly#Satellite_assemblies :))


La tarea 'solution' a su vez contiene varios parámetros definidos tanto por medio de atributos XML como de etiquetas hijo:


  • Atributos XML:

    • configuration: especifica la configuración a aplicar a la hora de generar el código (Release, Debug), de la misma manera que podemos hacer en Visual Studio
    • solutionfile: indica la ruta donde se encuentra el fichero de solución de Visual Studio. No es necesario que sea un path absoluto, puede ser relativo a la ruta donde se encuentra ubicado el fichero .build

  • etiquetas hijo:

    • projects: define una lista de ficheros de proyecto a generar. Por defecto la tarea 'solution' genera todos los proyectos definidos en el fichero de solucion, aunque podría ser necesario especificar que sólo genera un subconjunto de ellos.
    • excludeprojects: define una lista de ficheros de proyecto a excluir de la generación
    • referenceprojects: define una lista de ficheros que son necesarios referenciar para la generación de la solución
    • echo: muestra el texto especificado con el atributo 'message' en la consola


Una vez terminado, sólo tendremos que ejecutar nant desde la línea de comandos especificando el target (si sólo hay un fichero de build en el directorio, nant lo usa automáticamente):


nant compile


Y automágicamente el proyecto se generará ;)


Como veis esta "simple" tarea reemplaza al comando build de visual studio. Puede parecer una tontería teniendo ya disponible el comando en el editor, pero la idea es que podamos generar el proyecto entero sin necesitar que Visual Studio esté ejecutándose (aunque NAnt requiere que el SDK de .NET, MSBuild u otras herramientas estén instaladas en el sistema para ser capaz de ejecutar según qué tareas )


La idea es construir un fichero de build para NAnt que realice las tareas necesarias para nuestro proyecto. Por ejemplo, esto es lo que se muestra cuando ejecutamos el target tarea "Help" (que es la tarea por defecto) sobre mi fichero de configuración:

Buildfile: file:////ProjectGenerator/buildfile.build
Target framework: Microsoft .NET Framework 3.5
Target(s) specified: Help

[tstamp] lunes, 15 de diciembre de 2008 23:28:49.
[tstamp] build-process.date = 15-12-2008 23h 28m 49s.
[echo] [INFO] Including external files:
[echo] -> development-tree.definition.buildinclude
[echo] -> external-tool.paths.buildinclude
[echo] -> project.properties.buildinclude
[echo]
[echo] [INFO] Performing checks...
[echo] --> Checking development tree properties
[echo] --> Checking project properties...
[echo] [OK] All checks passed
[echo]
[echo] [INFO] Using default solution file:
[echo] -> AutomatedProjectGenerator.sln
[echo]
[echo] [INFO] Using default versioning file:
[echo] -> version.number
[echo]
[echo] [INFO] Using default guid file:
[echo] -> guid.number
[echo]
[echo] [INFO] Using default assemblyl file:
[echo] -> CommonProjectAssembly.cs
[echo]
[echo] [INFO] Using tests reports file:
[echo] -> UnitTestsReports
[echo]

Help:

[echo] -----------------------------------------------------
[echo] 'AutomatedProjectGenerator' build file Targets
[echo] -----------------------------------------------------
[echo]
[exec]
[exec] Skeleton file for the build process
[exec]
[exec] Default Target:
[exec]
[exec] Help - Lists the available targets in the build file
[exec]
[exec] Main Targets:
[exec]
[exec] build - Builds project and runs unit tests, placing the results in the publish directory
[exec] build-debug - Like the build target, but using the debug configuration when building
[exec] clean - Cleans up the build environment
[exec] CodeAnalisys - Analyses the generated assemblies using FXCop
[exec] Compile - Compiles the project
[exec] GenerateDoc - Generate documentation files
[exec] GenerateGUID - Generates a new GUID for the project
[exec] GenerateReports - Generate unit tests and coverage reports
[exec] Get - Grabs the code from the repository
[exec] Help - Lists the available targets in the build file
[exec] IncreaseBuildNumber - Updates project's version build number
[exec] IncreaseMajorNumber - Increases project's version major number
[exec] IncreaseMinorNumber - Increases project's version minor number
[exec] MoveToLastBuild - Moves last generated build files to the publish directory
[exec] publish - Zips all assets generated in the publish directory, getting it ready to be deployed
[exec] RepositoryCleanup - Issues a 'cleanup' command to the repository
[exec] RunTests - Run unit tests
[exec] UpdateDataAssembly - Updates data assembly file and commits it to the repository
[exec] UpdateVersionFile - Updates the versioning file and commits it to the repository
[exec]
[exec] Sub Targets:
[exec]
[exec] CloseLogs
[exec] CommitDir
[exec] ComputeIncludeList
[exec] Failure
[exec] StartLogs
[exec] Success
[exec]
[exec]
[echo] [INFO] Subtargets should not be called by the user

CloseLogs:


Success:

[echo] [OK] BUILD SUCCEDED See build.lastbuild.log for details

BUILD SUCCEEDED

Total time: 1.1 seconds.
En posteriores entradas describiré un poco cómo he organizado la estructura de directorios para un proyecto y colgaré instrucciones y todo lo necesario para utilizar la configuración de nant que he creado, en caso de que a alguien le interese.

3 Comentarios:

Zorro dijo...

Ahí, ahí, automatizando las tareas repetitivas para ser más eficaz. Muy ingenieril, sí señor.

Yo no distribuiría demasiado las herramientas, por eso de que cuando tienes armamento que los demás no tienen, juegas con ventaja ;). Todo el mundo dirá "¿Cómo hace este tío para compilar tan rápido?" y tú responderás "Dedos mágicos, nena"... XD

Ah, por cierto, el nuevo enlace a mi blog es http://zorrisimo.blogspot.com, para que lo actualices en la barra lateral.

Ricky dijo...

Blog actualizado. Peazo peluco te marcas ahora, proclamo :)

Tengo que continuar con esta serie y si, publicar las herramientas, al fin y al cabo uso software libre y es de bien nacido ser agradecido :)
El caso es cuándo tendré tiempo para hacerlo ya que la vorágine navideña me ha afectado: demasiadas tareas que dejé para hacer en vacaciones + estado físico algo chungo = blog desatendido.

El año que viene tengo en propósito cuidar un poco mejor de este cascarón y recuperar un poco la forma física, que este año la he dejado muy mucho :(

Zorro dijo...

El peluco muy bonito, pero lo otro que me han regalado... vamos, es prudente no dar más detalles XD.

Del software libre no hay nada que agradecer. Maldito ReiserFS... primero acaba con mi sistema de ficheros, luego vuelve a hacerlo un mes después, y al final el desarrollador principal va y mata a su mujer. ¡¡Por dios!! ò_ó

A ver si en 2009 te pones como ~Conan();