23 octubre 2012

git, Makefiles y versiones

Este día les traigo una entrada que es más que todo didáctica, esto lo digo porque estoy seguro que más de alguno va a decir: Mario, ¿No podías encontrar una forma más complicada de hacer esto?. Sin embargo para aquellos que utilizan git, esa entrada les podría servir como una muy breve introducción a algunas herramientas muy útiles en linux como lo son grep, cut y a la herramienta make que sirve para automatizar secuencias de comandos en general.

El problema...

Digamos que un día están trabajando su proyecto local y están haciendo sus commits con git. Cuando terminan de agregar nuevas características y su gran equipo de desarrollo de una sola persona (ustedes) está feliz y contento con el resultado, deciden empaquetarlo, quitar el versionado y hacer un archivo zip al estilo de MiSuperProyecto-v1.0.zip

El problema es que si son perezosos como yo, no querrán estar cambiando el número de versión cada vez que hagan un nuevo cambio a su proyecto. ¿Qué tal si inventamos un poco y hacemos un sencillisimo Makefile que nos permita hacer nuestros ZIP o tarball con nuestro número de versión especificado en el README.TXT?

¿Cómo archivar en git?

Haciendo uso del que todo lo sabe (osea Google) encontramos que el comando para archivar en git es el siguiente:

Para hacer un zip:

git archive --format=zip > MiSuperProyecto-v1.0.zip

Para hacer un tar.gz:

git archive --format=tar.gz > MiSuperProyecto-v1.0.tar.gz
Asumamos ahora que tenemos un README.TXT y dentro del readme podemos tener algo como esto:

README.TXT

Mi Super Proyecto
Version: 1.0

Esta es la version 1.0 del super proyecto Mi Super Proyecto.
¿Y que tal, si les digo que soy tan perezoso que quiero únicamente escribir algo como lo siguiente?
make publicar-zip
Y que automágicamente me genere mi archivo MiSuperProyecto-v1.0.zip

Conciendo a grep y cut

Primero, lo que queremos hacer es obtener el número de versión, para ello vamos a utilizar una convención muy simple: "Vamos a considerar el numero de versión a la línea dentro del archivo 'README.TXT' que contenga la palabra Version: seguido de un espacio y de un numero de versión compuesto dígitos separados por puntos".

Bajo esta definición números de versión válidos, serían similares a los siguientes:
1.0
1.1
1.2.1
47.2.1123 (<-- Esta será la versión de Firefox antes de fin de año)
Para poder identificar la línea de texto podemos utilizar en linux algo como GREP, grep es un programita muy sencillo, lee un archivo de texto y muestra las líneas que coincidan con la condición especificada. Para nuestro caso vamos a utilizar una expresión regular para definir el texto que queremos marcar.

No voy a usar la notación de expresión regular inicialmente pero para que nos hagamos una idea nuestro texto a buscar sería algo así:
Version: [numero](.[numero])[/numero][/numero]
La parte que aparece entre paréntesis se puede repetir 1 o N veces.

Traduciendo esa especificación a una expresión regular quedaría algo así:
^Version:\s*[0-9]+(\.[0-9]+)+
Siendo:
  • ^: Inicio de linea.
  • "Version:": El texto que quiero identificar luego del inicio de línea.
  • \s*: Un espacio en blanco que aparece 0 o N veces.
  • [0-9]+: Un dígito que aparece 1 o más veces.
  • (\.[0-9]+)+: Lo que está dentro del parentesis se repite 1 o N veces.

Y lo que aparece dentro del paréntesis:
  • \.: Un punto. (Sí... otro punto)
  • [0-9]+: Un dígito que aparece 1 o más veces.

En este caso vamos a usar la consola para probar nuestro "Match" asi que tenemos que modificar nuestra expresión a algo que entienda GREP:
^Version:\s*[0-9]\+\(\.[0-9]\+\)\+
Una vez hecho esto podemos probar nuestro comando asegurandonos de estar en la carpeta de nuestro proyecto y que el README.TXT exista en la carpeta raíz.

Que es lo mismo que la versión anterior pero con los caracteres escapados.
grep '^Version:\s*[0-9]\+\(\.[0-9]\+\)\+' README.TXT
Por último vamos a hacer uso de otro programa simpático llamado cut, cut como su nombre en inglés lo indica corta cadenas utilizando los caracteres que especifiquemos como separador.

Si bien habiamos dicho GREP nos devuelve lineas completas, cut nos ayudara a extraer el numero de versión.

Vamos a usar el caractér | para redireccionar la salida de grep hacia cut. Nuestro comando nos quedaría algo así:
grep '^Version:\s*[0-9]\+\(\.[0-9]\+\)\+' README.TXT | cut -d' ' -f2
Donde -d' ' especifica que nuestro separador es un espacio en blanco y -f2 le indica que queremos la segunda pieza de nuestra cadena. Obviamente la primera pieza va a ser la palabra "Version:".

Si todo ha salido bien, se nos debería de mostrar en la consola lo siguiente:
1.0

El Makefile I - Definiendo nuestro entorno

Los Makefile son archivos que combinan diferentes comandos para obtener diferentes resultados. El comando make lee el archivo Makefile del directorio a donde se esté ejecutando y luego busca el "objetivo" que el usuario desee crear.

Por ejemplo si yo estoy dentro de un directorio y escribo:
make compilar
El programa make entonces buscará el archivo Makefile, lo abrira y buscara la línea que se llame compilar, luego de esto ejecutará los comandos asociados a ese objetivo que pueden ser otros objetivos o simples compandos de la consola.

Lo bonito de make es que antes de ejecutar cualquier comando revisa si el archivo que deseamos crear ya exíste y si es afirmativo entonces nos dice: "Tu tonto, que no hay nada que hacer... deja de molestar."

Es broma, make no les va decir tontos. Pero si no hay nada que hacer obviamente se negará a trabajar hasta que eliminemos el archivo objetivo ya sea con otro comando a travez de make o de forma manual.

Lo primero que haremos es hacer un archivo Makefile en el editor que les paresca mas bonito, yo usaré VIM, ustedes pueden usar lo que más les guste:

Comenzaremos por definir algunas variables. ¿Recuerdan que queríamos el número de versión? Aquí es donde nos resultan útiles los comandos grep y cut.

Vamos a definir una variable llamada VER dentro de nuestro makefile que guardará nuestro numero de versión leído desde el README.TXT:
VER = $(shell grep '^Version:\s*[0-9]\+\(\.[0-9]\+\)\+' README.TXT | cut -d' ' -f2)
Pueden notar como utilizamos $(...) esa sintaxis nos permite ejecutar todo lo que escribamos dentro de los parentesis y nos devuelve el resultado como texto, para los makefiles hacemos uso de la palabra "shell" para indicarle que ejecutará comandos en la consola.

Luego de esto estableceremos una variable a la cual llamaremos "BASE", que nos servira como carpeta raíz para guardar los archivos de nuestro proyecto.
BASE = MiSuperProyecto
Una variable que construiremos será el nombre del archivo en zip que contendrá la versión que obtuvimos de correr el comando grep y cortar la salida con cut.
ZIPFILE = $(BASE)-v$(VER).zip
Y luego una última variable para el nombre del archivo en tar.gz.
TGZFILE = $(BASE)-v$(VER).tar.gz
Hasta este momento nuestro Makefile se deberá de ver algo así:
VER = $(shell grep '^Version:\s*[0-9]\+\(\.[0-9]\+\)\+' README.TXT | cut -d' ' -f2)
BASE = MiSuperProyecto
ZIPFILE = $(BASE)-v$(VER).zip
TGZFILE = $(BASE)-v$(VER).tar.gz

El Makefile II - Definiendo targets y ejecutando comandos

El funcionamiento de make es bastante trivial, cuando ustedes ejecutan en la linea de comando make haz-algo el busca en el archivo Makefile si hay algun "target" (objetivo) definido con ese nombre, este objetivo puede ser o un archivo o un conjunto de comandos. vamos a definir primero un par de objetivos bastante generales
publicar: publicar-zip

publicar-zip: out/$(ZIPFILE)
publicar-tgz: out/$(TGZFILE)
¿Notan como hemos reutilizado las variables que definimos al principio?

Traduciendo al español diría algo como lo siguiente: "El objetivo publicar requiere del objetivo publicar-zip; El objetivo publicar-zip requiere del archivo zip que esta en la carpeta out; El objetivo publicar-tgz requiere del archivo tar.gz que esta en la carpeta out

Hasta aquí todo bien, ¿Pero como hace make para saber como crear esos archivos? Pues eso es lo que vamos a definir ahora y lo "mágico" de make. Nuestros objetivos pueden utilizar los nombres de las variables de definimos al principio. Así que podemos hacer algo como lo siguiente:
out/$(ZIPFILE):
  git archive HEAD --format=zip --prefix=$(BASE)/ > out/$(ZIPFILE)

out/$(TGZFILE):
  git archive HEAD --format=tar.gz --prefix=$(BASE)/ > out/$(TGZFILE)
Noten que agrego el modificador --prefix para que dentro del zip o el tarball queden guardados los archivos dentro de una subcarpeta
Para los que se perdieron un poco, este makefile hará lo siguiente:
  1. Buscará el numero de versión en nuestro archivo Makefile
  2. Ejecutará el archivo correspondiente según si queremos hacer un zip o un tgz utilizando el número de versión que ya habíamos definido

Probando el Invento

Si todo ha funcionado bien, nos aseguramos de tener nuestro Makefile en nuestra de proyecto, nuestro README.TXT, nos aseguramos de que exista la carpeta "out" y corremos el siguiente comando:
$ make publicar
Deberíamos tener una salida como la siguiente:
git archive HEAD --format=zip --prefix=MiSuperProyecto/ > out/MiSuperProyecto-v1.0.zip
Si llamamos a "publicar" nos genera un zip porque es el objetivo por defecto que creamos en nuestro Makefile, pero podemos llamar al tgz si así quisieramos
$ make publicar-tgz
git archive HEAD --format=zip --prefix=MiSuperProyecto/ > out/MiSuperProyecto-v1.0.tar.gz
Lo bonito con make es que una vez hemos creado los archivos si intentamos correr el comando de nuevo, nos mostrará algo como lo siguiente:
$ make publicar-tgz
make: Nothing to be done for `publicar-tgz'.
make se niega a trabajar porque ya existe un archivo para la versión. ¿Realmente queremos sobreescribirlo? Puede que no, pero que tal si modificamos la versión y hacemos commit a nuestro repositorio, por ejemplo cambiamos nuestro readme a la versión 1.1 y hacemos git commit -a
Mi Super Proyecto
Version: 1.1

Esta es la version 1.1 del super proyecto Mi Super Proyecto.
al correr el siguiente comando
$ git commit -a
[master 09a31b8] Cambio de version
 2 files changed, 4 insertions(+), 4 deletions(-)
$ make publicar-tgz
git archive HEAD --format=tar.gz --prefix=MiSuperProyecto/ > out/MiSuperProyecto-v1.1.tar.gz
Esta vez no se niega a correr porque supone que estamos en una versión diferente y creará el archivo correspondiente, todo esto sin necesidad de modificar el Makefile. podemos verificarlo haciendo un ls -l out/*gz
 ls -l out/*gz
-rw-rw-r-- 1 mxgxw users 5288 Oct 23 20:50 out/MiSuperProyecto-v1.0.tar.gz
-rw-rw-r-- 1 mxgxw users 5290 Oct 23 20:54 out/MiSuperProyecto-v1.1.tar.gz
Espero les haya gustado esta GRAAN perdida de tiempo ;), Por último les dejo el Makefile completo para que lo modifiquen a su gusto

README.TXT

Mi Super Proyecto
Version: 1.1

Esta es la version 1.1 del super proyecto Mi Super Proyecto.

Makefile

VER = $(shell grep '^Version:\s*[0-9]\+\(\.[0-9]\+\)\+' README.TXT | cut -d' ' -f2)
BASE = MiSuperProyecto
ZIPFILE = $(BASE)-v$(VER).zip
TGZFILE = $(BASE)-v$(VER).tar.gz

publicar: publicar-zip

publicar-zip: out/$(ZIPFILE)
publicar-tgz: out/$(TGZFILE)

out/$(ZIPFILE):
  git archive HEAD --format=zip --prefix=$(BASE)/ > out/$(ZIPFILE)

out/$(TGZFILE):
  git archive HEAD --format=tar.gz --prefix=$(BASE)/ > out/$(TGZFILE)