domingo 21 de diciembre de 2008

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

4 Comentarios

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.

domingo 14 de diciembre de 2008

IronPython & IronPython Studio

0 Comentarios

El pasado 10 de diciembre se lanzó la versión 2.0 de IronPython, la implementación del conocido lenguaje dinámico Python que corre bajo .NET. Esto permite, por ejemplo, que podamos acceder a todas las librerías que ofrece el framework.NET utilizando python, lo que unido a sus capacidades como lenguaje de script, puede ser una herramienta muy interesante de cara a desarrollar prototipos o aplicaciones simples. También para todo tipo de aplicaciones supongo, pero en mi caso no domino muy bien el lenguaje como para eso :)

Una de las novedades destacadas es que ahora utiliza el Dynamic Language Runtime lo que le da, por ejemplo, ventajas de cara a la interoperabilidad, algo ya comentado cuando hablé de las mejoras que se esperan para c# 4.0, así como importantes mejoras de rendimiento. También como novedad, el código está disponible como código abierto bajo la Microsoft Public License.

La versión 2.0 de IronPython es compatible con CPython 2.5, que no es la última versión existente del lenguaje, ya que hace escasamente 10 días que salió la versión 3.0 de éste último. Sin embargo la versión 3.0 de Python no es compatible con las anteriores al incluir nueva funcionalidad y además reorganizar las librerías estándar del lenguaje, así que a día de hoy la versión 2.5 es la que más base de código tiene.

Relacionado con este proyecto tenemos IronPython Studio, que utiliza Visual Studio 2008 para propocionar un IDE para IronPython, incluyendo plantillas de proyecto iniciales, resaltado de sintaxis y autocompletado de código. Para la instalación podemos elegir entre integrarlo con una instalación ya existente de Visual Studio, o bien instalarlo como un componente independiente.

IronPythonIDE

El único problemilla que tiene actualmente es que sigue basado en IronPython 1.0, así que habrá que esperar a que se actualice a la nueva versión.

lunes 8 de diciembre de 2008

Links de la sem... de cuando buenamente pueda

1 Comentarios

Bueno, pues más que links de la semana mejor lo empezaré a denominar F.A.I.L (Fun, Aperiodic and Interesting Links)

No hay muchos links porque me he dado cuenta que la lista de libros que estoy leyendo no ha variado un ápice desde que abrí el blog, así que en vez de mover la rueda del ratón para hacer scroll, me he puesto a pasar páginas

lunes 1 de diciembre de 2008

PDC 2008: The Future of C# 4.0 (3/3)

0 Comentarios

Y llegamos a la tercera y última entrada de esta mini-serie, donde hablaremos de otra de las características presentadas para C# 4.0: covarianza y contravarianza en tipos genéricos.

Primero explicaremos brevemente lo que es la covarianza y la contravarianza, o al menos simplificar un poco la reseña en la wikipedia ;)

En POO, una jerarquía de herencia puede verse como una relación de órden. Si tenemos por ejemplo esta relación de clases:

example

y si definimos la relación A ≤ B significando "B es un A" o que "B es más especializado que A", vemos que cumple una relación matemática de orden parcial (cumple las propiedades reflexiva, antisimétrica y transitiva).

Una vez dicho esto, los términos covarianza y contravarianza, se refieren al tipo de sustitución que podemos hacer, siendo B subtipo de A, y podemos entenderla así:

Covarianza A ≤ B Podemos sustituir un tipo A por otro más especializado B
Contravarianza A ≥ B Podemos sustituir un tipo A por otro más genérico B

Esto lo veremos mejor con ejemplos: considerando la jerarquía anterior, sea un método definido:

B MyMethod(B parameter);

Las llamadas a métodos son covariantes tanto en sus valores de retorno como en el paso de parámetros; es decir podemos retornar un tipo B, o cualquier subtipo suyo, y podemos pasar como parámetro un tipo B, o cualquier subtipo:

void MyMethod(B parameter) { return new D(); }
...
MyMethod( new D() );

Sin embargo en los delegados tenemos contravarianza en el paso de parámetro, es decir dado este delegado la asignación es legal

delegate void MyDelegate(D parameter);
MyDelegate myDelegate; //delegate instance
myDelegate += MyMethod;


La segunda asignación es válida porque en realidad el delegado está especificando que acepta uno de los (potencialmente muchos) subtipos que acepta el método, con lo que estamos aplicando una restricción mas fuerte. Por tanto estamos asignando MyMethod, que recibe un parámetro de tipo B, a un delegado que espera un tipo D, es decir, sustituimos un tipo concreto por otro más genérico.


El problema es que todo esto no se aplicaba con herencia cuando usamos tipos genéricos, es decir estos eran invariantes. No podíamos hacer esto:

public interface IMyInterface<TYPE> { ... } 
public static class Tmp
{
public static IMyInterface<B> GetData()
{
return new B[] {new B(), new B()};
}
}

IMyInterface<B> e1 = Tmp.GetData();
IMyInterface<A> e2 = e1; // ERROR

Esto es a priori perfectamente válido, ya que A es más genérico que B, sin embargo no es una asignación válida en C# al carecer de covarianza en valores de retorno para tipos genéricos. Cuando tengamos disponible C# 4.0, podremos indicar que un tipo genérico concreto admitirá covarianza en valores de retorno por medio de la palabra clave out y declarando el tipo genérico :

public interface IMyInterface<out TYPE> { ... }
Para utilizar contravarianza con genéricos usaremos la palabra clave in. Nueva fusilada de la presentación:
public interface IComparer>in T<
{
int Compare(T x, T y);
}

IComparerobjComp = GetComparer(); //Returns an IComparer
IComparer strComp = objComp; //Ilegal before 4.0
Queda un punto muy interesante por tratar de la presentación, la novedad de presentar el compilador de C# como un servicio, es decir, podremos llamar al compilador de C# en tiempo de ejecución para compilar el código que queramos y que esté accesible en ese momento. Sin embargo esta característica es similar a una presentada en Mono, y la trataré en una entrada posterior :)