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.

Siempre he pensado que las herramientas que usamos en el desarrollo de software deben estar seleccionadas y adaptadas en función del tipo de desarrollo que realizamos, el equipo, la metodología, etc.

Sin embargo, hay tres herramientas básicas que uso en todos los proyectos: un sistema de control de versiones (desde git hasta subversion), un gestor de bugs/tareas (aunque sea una hoja Excel) y un servidor de integración continua.

Las dos primeras herramientas de la lista están muy extendidas y no conozco ningún equipo de desarrollo que trabaje sin ellas, pero lo del servidor de integración continua es menos conocido y no se usa tanto.

¿Qué es la integración continua?

La integración continua es una practica ligada a la programación extrema que consiste en automatizar el proceso de integración de código para poder realizarlo de la manera más continuada posible.

Cuando llega el momento de liberar una nueva versión de una aplicación hay que realizar un proceso de integración de todos los componentes que forman parte de la aplicación. Esto implica realizar varios pasos, como obtener el código fuente, compilarlo, ejecutar los tests, etc.

En muchos proyectos este es un proceso completamente manual, en el mejor de los casos documentado en forma de checklist en alguna parte, pero a fin de cuentas, siempre que haya pasos manuales, antes o después habrá algún error y pondremos en producción una aplicación con el número de versión incorrecto, que no ha pasado las pruebas automatizadas o que no estará correctamente etiquetada en el control de código fuente.

Si queremos mejorar este proceso, el primer paso es automatizarlo. Para ello contamos con diferentes herramientas, generalmente basadas en algún tipo de script de compilación. En .NET se suele utilizar MSBuild, en Javascript hay opciones como Grunt, en Ruby existe Rake… Al final todas se basan en lo mismo, poder definir de una manera más o menos cómoda las tareas a ejecutar para poder lanzarlas en un único paso.

Un ejemplo de script de compilación para una aplicación de escritorio podría contar con los siguientes pasos:

  1. Obtener la última versión del código fuente.
  2. Actualizar la información de versión en el fichero AssemblyInfo.cs basándose en la convención que se haya establecido (número de compilación, fecha, etc.).
  3. Ejecutar una herramienta de análisis estático como FxCop o SourceMonitor para detectar defectos en el código.
  4. Compilar los proyectos.
  5. Ejecutar las suites de tests automatizados.
  6. Generar la documentación (manuales, guías de instalación, etc.).
  7. Crear los paquetes de instalación.
  8. Etiquetar en el control de código fuente la versión del código usada para generar estos instaladores, de forma que luego podamos trazar exactamente qué codigo fuente se uso para generar cada versión de la aplicación.

Preparar este script puede llevar un tiempo y no es una tarea fácil la primera vez que lo haces, pero una vez que has aprendido es bastante sencillo adaptarlo para nuevos proyectos, por lo que la inversión de tiempo se recupera rápidamente.

¿Qué aporta un servidor de integración continua a este proceso?

Una vez que contamos con este script, cada vez que queramos realizar una integración completa (potencialmente liberable) del código, podremos hacerlo en un sólo paso ejecutándolo desde cualquier equipo; entonces, ¿para qué hace falta un servidor de integración continua?

El servidor de integración continua se encarga de ejecutar este script automáticamente según la programación que establezcamos, por ejemplo cada vez que se se realiza un commit/checkin/push al control de código fuente.

Además, podemos establecer distintas configuraciones para un mismo proyecto. Por ejemplo, podemos crear una configuración que realice una compilación “rápida”, ejecutando sólo los test unitarios cada vez que realizamos un commit, y programar una compilación “completa”, que ejecuta también los tests de integración y genera los instaladores todas las noches (las típicas nightly builds).

Realizar el proceso de integración de continuamente en un servidor tiene varias ventajas:

Por una parte, evita discrepancias entre las versiones utilizadas para generar los entregables. Al realizar la compilación en el servidor se garantiza que siempre se usa la misma versión del compilador, SDKs, librerías de terceros, etc. Eso es difícil de conseguir si la compilación se realiza cada vez en equipo distintos donde podría haber distintas versiones de cosas instaladas globalmente (assemblies en el GAC, módulos node instalados con npm install -g, etc.), provocando cambios en el comportamiento final de la aplicación dependiendo del equipo en que se ha compilado. Este tipo de errores son complicados de diagnosticar y pueden hacerte perder mucho tiempo.

Además, permite detectar rápidamente errores introducidos por cambios en el código. Aunque se supone que antes de hacer un commit cada desarrollador debería asegurarse de que todo funciona y ejecutar los tests en su equipo, a veces las suites de tests tardan más tiempo del que nos gustaría en ejecutarse (especialmente si tenemos tests de integración) o directamente al desarrollador se le olvida. Al hacer que el código se compile cada vez que se hace el commit en el servidor de integración continua, los errores se detectan en cuestión de minutos en lugar de hacerlo varios días después cuando alguien intenta liberar una nueva versión.

Otra ventaja de usar un servidor de integración continua es que podemos mantener un histórico de los resultados de las compilaciones. Dependiendo de los análisis que estemos realizando sobre nuestro código, esto nos permite conocer como ha evolucionado la cobertura de código de los tests, la complejidad ciclomática media y todas esas métricas que tanto nos gustan (otra cosa es que sirvan para algo, pero eso lo dejamos para otro día).

En teoría, si este proceso se lleva al extremo cada vez que se hace un commit al control de código fuente el servidor de integración continua sería capaz de detectar si todo es correcto y en ese caso hacer un despliegue automático. Eso es lo que se conoce como entrega continua (continuous delivery o continuous deployment) y hay proyectos que son capaces de hacerlo, aunque no siempre es viable (ni recomendable).

Conclusiones

Montar un servidor de integración continua puede resultar un poco complicado al principio. Hay muchas opciones disponibles, algunas más sencillas y otras más complejas, pero donde realmente reside la complicación es en estructurar tu proyecto para que todas las tareas sean fácilmente automatizables.

Aunque pueda parecer que este tipo de técnicas sólo son aplicables a equipos muy grandes que hacen aplicaciones muy complejas, no es así. Yo lo he utilizado en proyecto con un único desarrollador en los que lo único que se hacía era compilar el código y generar un instalador, y aun así merece la pena. Tener la seguridad de que lo que has compilado es lo que se debía compilar, que la versión está correctamente asignada y que el instalador lo has generado con los binarios correctos (y no con otros que compilaste dos horas atrás), te permite vivir mucho más tranquilo.

Es verdad que el coste inicial es alto, pero lo bueno es que las técnicas necesarias son muy fáciles de reutilizar de un proyecto a otro, por lo que una vez que has automatizado la integración del primer proyecto, si sigues una estructura común en todos tus proyectos, el resto es cuestión de ajustar un par de rutas en los scripts de compilación y los tienes listos en minutos (literalmente).

Hace poco me entretenía con la enésima discusión en twitter sobre las maravillas y desastres de node.js y acabó saliendo un tema que hacía tiempo que no veía: el uso de librerías y plataformas de código abierto.

Sobre esto del código abierto hay posiciones muy extremas, tanto en un sentido como en otro (me atrevería a decir que hay mayor extremismo entre los defensores del código abierto), pero dejando a un lado fanatismos, creo que es posible realizar un análisis más pragmático del asunto.

OJO: no voy a entrar en discusiones ético-filosóficas sobre las bondades de uno u otro sistema. Tampoco voy a meterme en las diferencias entre código abierto, software libre y software gratis, porque aunque creo que son importantes, prefiero mantener un enfoque más “práctico” en este post.

Un mundo guiado por un único fabricante

Los que trabajamos con plataformas de Microsoft hemos estado muy acostumbrados a utilizar únicamente lo que nos proporcionaba Microsoft que, por otra parte, no era poco y cubría bastante bien la mayoría de situaciones a las que nos teníamos que enfrentar.

Si necesitabas hacer una aplicación web, tirabas de WebForms (o ahora de ASP.NET MVC). Si querías un ORM, Entity Framework. Para bases de datos, SQL Server, y si tenía que ser embebida, SQL CE. Esa era la forma de hacer las cosas y mucha gente no se la cuestionaba.

Recuerdo que hace unos 6 años empecé a introducir en mi trabajo del mundo real librerías “no Microsoft”. Primero log4net, luego Castle Windsor, después NHibernate, …

Al principio tuve que vencer cierta reticencia entre algunos compañeros de trabajo, pero el tiempo me ha dado la razón y hoy en día usamos muchas librerías “de terceros”, la mayoría de ellas open source, y debo decir que han mejorado considerablemente nuestros procesos de desarrollo (tanto a nivel de productividad como a nivel de calidad).

Aún hoy hay empresas en que plantear usar tecnologías no Microsoft dentro de un proyecto .NET se considera una mala idea y existen directores técnicos que directamente vetan el uso de estas tecnologías.

Es como el viejo dicho de “nunca despidieron a nadie por comprar IBM“, parece que si te ciñes al uso de tecnologías soportadas por Microsoft, nada puede salir mal, o al menos si sale mal, la culpa no será de tu elección tecnológica.

Sin embargo, gracias a la exposición constante que tenemos los desarrolladores a otras comunidades y a la mezcla de ideas procedentes de otros lenguajes, ya no es tan raro emplear herramientas de código abierto en proyectos de .NET.

De hecho, en los últimos años la propia Microsoft ha abrazado ese concepto con herramientas con Nuget y las últimas librerías desarrolladas por Microsoft están siguiendo este modelo de desarrollo (ahí tenemos Entity Framework aceptando pull requests de desarrolladores externos como Unai).

Una cuestión de confianza

En realidad, el problema que tiene mucha gente con el software de código abierto no tiene nada que ver con que el código sea abierto o cerrado o con que la licencia y el modelo de desarrollo sea más o menos libre, sino con quién está detrás del código.

Existe una sensación demasiado habitual de que un proyecto de código abierto que no está dirigido por una organización comercial es algo en lo que no se puede confiar, pero el tiempo nos ha enseñado que esto no es cierto.

Hay proyectos realizados por voluntarios que llevan mucho tiempo funcionando bien y tienen bases de usuarios tan grandes que los convierten en soluciones mucho más sólidas que sus equivalentes comerciales. Los más extendidos han conseguido además el apoyo de grandes empresas, lo que les da ese plus de confianza que parece necesario para usarlos en ciertos entornos (estoy pensando en casos como jQuery).

Al final, lo más importante para que una librería o plataforma tenga un buen soporte es que cuente con un número suficiente de usuarios. Si una tecnología alcanza el suficiente grado de adopción, siempre será más fácil encontrar alguien dispuesto a soportarla, ya sea de manera altruista respondiendo preguntas en Stack Overflow o de forma comercial cobrando por servicios de consultoría.

En ese sentido, a la hora de decidir si una librería determinada o una plataforma concreta son confiables, me parece más importante tener en cuenta la base de usuarios que quién está detrás de ella. De hecho, hay ocasiones en que tener una empresa detrás puede llegar a ser contraproducente porque puede que mantener esa tecnología viva vaya en contra de los objetivos económico/estratégicos de la empresa (preguntádselo a los que decidieron usar Linq2SQL para ver cómo en poco tiempo era descontinuado y reemplazado por Entity Framework).

También es cierto que existen empresas capaces de garantizar la supervivencia de un producto y el soporte del mismo durante un tiempo prefijado. Ahí están casos como Canonical (una empresa tan “comercial” como Microsoft) y sus releases de Ubuntu con soporte extendido o los ciclos de vida que ofrece Microsoft para productos como SQL Server.

Conclusiones

Este post no pretende ser un alegato a favor del movimiento Open Source y en contra de Microsoft. Nada más lejos de mi intención. Como he dicho al principio, creo que hay demasiado fanatismo en ambos bandos y se tiende a perder de vista el objetivo real: generar valor desarrollando software.

A la hora de elegir sobre qué plataforma desarrollar o qué librería utilizar hay muchos factores a tener en cuenta y, sin duda, el soporte que podamos obtener es uno de ellos, pero también lo es la facilidad para desarrollar sobre ella, el rendimiento que podamos conseguir y la disponibilidad de buenos desarrolladores.

Los típicos argumentos de “código cerrado malo porque no lo puedo tocar”, “código abierto malo porque nadie me lo va a mantener” suelen caerse por su propio peso. Todos conocemos ejemplos de productos comerciales que han sido descontinuados dejando de dar soporte a sus usuarios, y todos usamos un montón de código abierto que no sabríamos tocar ni en un millón de años.

Lo que cuenta es resolver un problema y buscar las mejores herramientas para ello, que a veces serán open source y a veces no. No dejes que eso sea lo único que te guíe a la hora de elegir las herramientas más adecuadas para tu próximo proyecto. El código (abierto o cerrado) importa, pero el contexto más.

Conocer a nuestros usuarios es cada vez más importante para poder ofrecerles servicios más ajustados a sus necesidades.

Cuando estamos trabajando con una aplicacion web existen herramientas como Google Analytics que nos permiten recabar todo tipo de información (también podemos usar Google Analytics con aplicaciones de escritorio) , pero hay veces que necesitamos conocer detalles como la ubicación del usuario en el momento de interactuar con él, y no sólo a posteriori para realizar análisis estádísticos.

Una de las cosas que nos puede ayudar a mejorar la experiencia de usuario es conocer el lugar en el que se encuentra el usuario.

En HTML5 existe un api de geolocalización, pero no siempre está disponible y, además, requiere del consentimiento del usuario.

Otra opción es utilizar lo que se conoce como geolocalización de IP, que consisten en tratar de averiguar la ubicación del usuario basándonos en su dirección IP. Esto no es completamente fiable porque el usuario puede acceder a través de un proxy o usar una IP que está físicamente alejada de su ubicación real (la IP está realmente asociada al ISP no al equipo del usuario), pero en muchas ocasiones es suficiente.

Geolocalizando una IP

Para conocer la ubicación del usuario existen muchos servicios, pero en este post vamos a ver uno muy simple y, además, gratuito (con ciertos límites): IPInfoDB.

El servicio ofrecido por IPInfoIDB requiere que nos registremos para conseguir un API KEY, pero este registro es completamente gratuito.

Una vez completado el registro, el código necesario para geolocalizar una IP en C# es muy sencillo:

public Location GeoLocate(string ip)
{
    const string API_KEY = "YOUR_API_KEY";

    var url = "http://api.ipinfodb.com/v3/ip-city/?key={0}&ip={1}&format=xml";
	
    var doc = XDocument.Load(string.Format(url, API_KEY, ip));

    return new Location
    {
        CountryCode = doc.Descendants("countryCode").First().Value,
        CountryName = doc.Descendants("countryName").First().Value,
        Region = doc.Descendants("regionName").First().Value,
        City = doc.Descendants("cityName").First().Value,
        ZipCode = doc.Descendants("zipCode").First().Value,
        Latitude = decimal.Parse(doc.Descendants("latitude").First().Value, CultureInfo.InvariantCulture),
        Longitude = decimal.Parse(doc.Descendants("longitude").First().Value, CultureInfo.InvariantCulture)
    };
}

Como véis no hay mucha complicación, cargamos en un documento XML los datos devueltos por el servicio y construimos un objeto Location a partir de la información contenida en el documento XML.

No tan rápido

Antes de empezar a usar este código en tu próxima aplicación, hay que tener en cuenta algunas limitaciones:

  • El código anterior es código de demo. No hay ningún tipo de control de errores, y lo último que querrías es que tu aplicación dejase de funcionar porque el servicio de IPInfoDB está caído. Una buena opción cuando uno se integra con este tipo de servicios es aplicar el patrón disyuntor (circuit breaker).
  • Utilizar esta técnica para geolocalizar IPs requiere una llamada a un servidor externo, y esa llamada puede ser más lenta de lo que nos gustaría. Es importante tenerlo en cuenta y sería muy recomendable realizarla de forma asíncrona (y con un timeout, por cierto) para evitar bloquear nuestra aplicación eternamente.

Conclusiones

Geolocalizar una IP usando C# es muy sencillo con servicios como IPInfoDB y puede ayudarnos a hacer aplicaciones más inteligentes que resulten más agradables de utilizar para nuestros usuarios, pero no hay que olvidar que estamos introduciendo una dependencia sobre un servicio externo y hay infinidad de cosas que pueden salir mal, por lo que nuestra aplicación deberá estar preparada para tratar con estas contingencias.

Este fin de semana se ha celebrado en Santa Pola el evento #TuNodeYoXaml de Gusenet, el Grupo de Usuarios .NET del Sureste. Han sido un par de días memorables, llenos de charlas interesantes con una participación de primer nivel tanto entre los ponentes como entre los asistentes.

El buen ambiente ha sido la tónica general durante todo el fin de semana y creo que todos nos hemos ido de allí con un excelente sabor de boca.

Ha sido un verdadero placer conoceros personalmente a muchos de vosotros a los que sólo os conocía a través de twitter y vuestros blogs y, sobre todo, comprobar de primera mano que en este país hay mucha gente intentando hacer las cosas bien y, a la vez, disfrutando con su profesión.

Muchas gracias Oscar Montesinos (@2flores), Pedro Hurtado (@_PedroHurtado) y Xavier Jorge Cerdá (@XaviPaper) por el excelente trabajo que habéis hecho para organizar este evento y hacernos sentir a todos como en casa. Os merecéis el éxito que habéis tenido.

A título personal, no puedo dejar de agradecer a mis compañeros de viaje Luis Ruíz (@LuisRuizPavon), Sergio León (@panicoenlaxbox) y Carlos Duque (@ChkDuke) lo fácil que me lo han puesto todo y lo mucho que nos hemos divertido juntos.

¡Nos vemos en la próxima!