Ruby on Rails: Tutorial de Rake (o como me convertí el alcohólico)

Como contribución a la fabulosa y creciente comunidad Ruby, y dado que no soy un Ruby's Skilled Guru como para hacer artículos realmente extraordinarios, les dejo esta traducción del original Ruby on Rails Rake Tutorial (AKA How Rake turned me into an alcoholic) via RailsEnvy que he realizado en mis tiempos libres en estos días.

Como desarrollador Rails probablemente estás familiarizado con correr "rake" en tus test porque has usado "rake db:migrate" para correr tus migraciones. Pero ¿Realmente entiendes que es lo que sucede dentro de las tareas de Rake? ¿Sabías que puedes escribir tus propias tareas o crear tu propia biblioteca de útiles archivos Rake?
Aquí hay unos pocos ejemplos de como he usado tareas Rake

  • Obtener una lista de miembros para enviarles un email.
  • Hacer calculos de datos y reportes a última hora.
  • Expirar y regenerar caches
  • Hacer respaldos de mi base de datos y del repositorio subversion
  • Correr cualquie tipo de script de manipulación de datos.
  • Tomar tragos para ponerme borracho

En este artículo vamos a descubir la razón por la que Rake fue creado, y como esto nos puede ayudar en nuestras aplicaciones Rails. Al final, deberías poder escribir tus propias tareas, y aprender como mearte estando borracho en no menos de tres pasos.

Este artículo se encuentra disponible también en Francés, Ruso, Chino, y Polaco.

Tabla de contenidos

  1. ¿Por qué make?, una retrospectiva
  2. ¿Cómo obtuvimos Rake?
  3. ¿Cómo trabaja Rake?
  4. ¿Cómo trabajan las dependencias en Rake?
  5. ¿Cómo documento mis Tareas Rake?
  6. Espacios de nombre en Rake
  7. ¿Cómo escribo Tareas Rake útiles?
  8. ¿Cómo escribo las tareas Rake para mi aplicación Rails?
  9. ¿Puedo acceder a mis Modelos de Rails desde una Tarea?
  10. ¿Dónde puedo encontrar más ejemplos?

¿Por qué make?, una retrospectiva

Para entender porqué tenemos Rake, necesitamos primero dar una mirada a Make, el viejo abuelo de Rake.

Viaja conmigo por un momento hacia atrás, a los viejos días en los que cada pieza de código tenía que ser compilada, antes que los lenguajes interpretados y los iPhones llenasen la Tierra.

Volviendo entoces cuando descargas programas grandes ellos vienen como un montón de lineas de código fuente y un script shell. Este script shell debería contener cada línea de código necesitada por tu ordenador para poder compilar/enlazar/construir la aplicación. Deberías correr "install_me.sh" (el script shell), entonces cada línea de código correría (tipicamente compilando el código fuente), y deberías poder obtener un ejecutable que podrías ejecutar.

Esto ha funcionado bien para la mayoría de la gente, excepto que seas uno de los pocos desafortunados desarrolladores del programa. Cada vez que debías hacer un pequeño cambio al fuente y deseabas testearlo, debías tener que volver a ejecutar el script shell el cual recompilaría todo de nuevo. Obviamente esto trajo muchos problemas grandes por un largo tiempo.

En 1977 (el año en que nací) Stuart Feldman de Bell Labs inventó "Make", el cual resolvió el problema de esos largas compilaciones. Make se usa para compilar programa del mismo modo, pero con dos grandes avances:

  1. Make puede reconocer que archivos/fuentes han cambiado desde la última vez que la aplicación fue compilada. Usando esta información, la segunda vez que corra Make, solo compilará los archivos fuentes que fueron modificados.
  2. Make tambien contiene el tracking de dependencias por lo que le puedes decir al compilador que el "Archivo Fuente A" requiere del "Archivo Fuente B" para compilar apropiadamente, y que el "Archivo Fuente B" requiere del "Archivo Fuente C" para compilar tambien apropiadamente. Por esto, si Make quiere compilar "Archivo A" y si "Archivo B" no está compilado aún, el sistema compilará B primero.

También debería explicarse que "Make" es simplemente un programa ejecutable como "dir" o "ls". Para que make entienda como compilar un programa, se necesita crear un archivo "makefile" el cual receferencia a todo el fuente y las dependencias. los "makefiles" tienen su propia sintaxis críptica de la cual no necesitamos saber nada.

Make ha evolucionado a traves de los años y comenzaron a usarlos otros lenguajes de programación. De hecho, los programadores Ruby lo usaron antes que Rake llegase.

"Pero Ruby no es un lenguaje compilado, ¿Para qué lo usarían los programadores Ruby?" Escucho que exclamas.

Si, Ruby es un lenguaje interpretado y no necesitamos compilar nuestro códido.

¿Por qué razón hubieron programadores de Ruby usando archivos Make?

Bien, por dos razones:

  1. Creación de Tareas: Con cada aplicación grande casi siempre terminas escribiendo scripts que puedes correr desde la linea de comandos. Puedes querer limpiar el cache, correr una tarea de mantenimiento, o migrar la base de datos. Mas que llegar a crear 10 scripts shell separados (o uno grande y complejo), puedes crear un simple "Makefile" en el que puedas organizar las cosas sugún las tareas. Las tareas pueden correr tipeando algo como "hacer estupido" (el que correrá la tarea estúpida).
  2. Tareas deTracking de dependencias: Cuando empiezas a escribir una biblioteca de tareas de mantenimiento, comenzarás a notar que algunas tareas pueden se repiten parcialmente. Por ejemplo, la tareas "migrate" y "schema:dump" requieren una conexión a la base de datos. Puedo crear una tarea llamada "connect_to_database", y hacer que "migrate" y "schema:dump" dependan de "connect_to_database". Entonces la siguiente vez que corra "migrate", "connect_to_database" correrá antes que "migrate".

¿Cómo obtuvimos Rake?

Bien, hace unos años atrás Jim Weirich se encontraba trabajando en un proyecto Java en el que estaba usando Make. Mientras trabajaba con su Makefile se dio cuenta de cuan conveniente podía ser si pudiese escribir pequeños snippets de Ruby dentro de su makefile. Por lo que el creó Rake. Tuvimos la suficiente suerte de encontrarnos con Jim en RailsConf el mes pasado, y realmente es un tipo agradable.

Jim incorporó la habilidad de crear Tareas, hacer tracking de dependencias de tareas, y tambien incorporar reconociento de timestamp (reconstrucción unicamente de archivos modificados desde la última compilación). Obviamente esta característica no es usada frecuentemente, dado que no compilamos.

Siempre me soprendí de lo que "Jim Weirich" hizo, y ahora lo conozco también! Jim nunca tuvo intenciones de escribir este código, supongo que nació de el.

¿Cómo rake hace el trabajo?

Inicialmente quise titular a esta sección "Como quedar tirado con Rake", pero claro, esto no es muy intuitivo.

Digamos que quiero emborracharme, ¿Qué pasos necesitarían involucrarse?

  1. Comprar alcohol
  2. Mezclarme un trago
  3. Alcanzar la borrachera

Si yo quiero usar Rake para llamar a cada una de estas tareas, debería crear un archivo llamado "Rakefile" el que contendría algo así:

  1. task :comprarAlcohol do
  2. puts "Vodka comprado"
  3. end
  4. task :mezclarTrago do
  5. puts "Caipiroska mezclada"
  6. end
  7. task :estarDescerebrado
  8. do
  9. puts "Uhh loco..., veo elefantes rosadoooossss"
  10. end

Entonces puedo correr estas tareas desde el mismo directorio que mi archivo rake, algo como esto:

  1. $ rake
  2. comprarAlcohol
  3. Vodka comprado
  4. $ rake mezclarTrago
  5. Caipiroska mezclada
  6. $ rake estarDescerebrado
  7. Uhh loco..., veo elefantes rosadoooossss

Muy bien! Sin embargo, desde el punto de vista de las dependencias, yo podría correr estas tareas en cualquier orden. Algunas veces yo podria "estarDescerebrado" antes que yo "mezclarTrago" o "comprarAlcohol", esto es humanamente imposible.

¿Cómo expreso las dependencias con rake?

  1. task :comprarAlcohol do
  2. puts "Vodka comprado"
  3. end
  4. task :mezclarTrago => :comprarAlcohol do
  5. puts "Caipiroska mezclada"
  6. end
  7. task :estarDescerebrado => :mezclarTrago do
  8. puts "Uhh loco..., veo elefantes rosadoooossss"
  9. end

Ahora estoy diciendo que "para mezclarTrago, primero debo comprarAlcohol", y "para estarDescerebrado debo mezclarTrago". Como puedes esperar, las dependecias se apilan. Veamos:

  1.  
  2. $ rake comprarAlcohol
  3. Vodka comprado
  4. $ rake mezclarTrago
  5. Vodka comprado Caipiroska mezclada
  6. $ rake estarDescerebrado
  7. Vodka comprado
  8. Caipiroska mezclada
  9. Uhh loco..., veo elefantes rosadossss

Como puedes ver, cuando ahora voy a "estarDescerebrado", son llamadas las tareas dependientes "comprarAlcohol" y "mezclarTragos". Después de un tiempo puedes ser tentado a expandir tus adicciones, y por lo tanto expandir tu Rakefile. También puedes tentarte a convertir en adictos a tus amigos. Como en un proyecto de software real, cuando agregas gente a tu equipo, necesitarás asegurarte que tienen una buena documentación. La pregunta consiguiente es:

¿Cómo documento mis tareas Rake?

Aquí está como lo usarías.

  1.  
  2. desc "Esta tarea comprará tu Vodka"
  3. task :comprarAlcohol do
  4. puts "Vodka comprado"
  5. end
  6. desc "Esta tarea mezclará un buen cocktail"
  7. task :mezclarTrago => :comprarAlcohol do
  8. puts "Caipiroska mezclada"
  9. end
  10. desc "Esta tarea beberá demasiado"
  11. task :estarDescerebrado => :mezclarTrago do
  12. puts "Uhh loco..., veo elefantes rosadossss"
  13. end

Como puedes ver, cada una de mis tareas ahora tiene un "desc". Esto nos permite, tanto a mi como a mis amigos, escribir "rake -T" o "rake --tasks"

  1. $rake --tasks
  2. rake estarDescerebrado # Esta tarea beberá demasiado
  3. rake mezclarTrago # Esta tarea mezclará un buen cocktail
  4. rake comprarAlcohol # Esta tarea comprará tu Vodka

Bastante fácil, no?

Nombres de espacio de Rake

Una vez que te hayas vuelto un alcoholico y que estés usando un montón de tareas Rake, puedes necesitar una mejor manera de categorizarlas. Aquí es donde los espacios de nombres entran en acción. Si yo usase espacios de nombres en el ejemplo de arriba, puede lucir algo así.

  1. namespace :alcoholic do
  2. desc "Esta tarea comprará tu Vodka"
  3. task :comprarAlcohol do
  4. puts "Vodka comprado"
  5. end
  6. desc "Esta tarea mezclará un buen cocktail"
  7. task :mezclarTrago => :comprarAlcohol do
  8. puts "Caipiroska mezclada"
  9. end
  10. desc "Esta tarea beberá demasiado"
  11. task :estarDescerebrado => :mezclarTrago do
  12. puts "Uhh loco..., veo elefantes rosadossss"
  13. end
  14. end

Los espacios de nombres te permiten agrupar tareas de acuerdo a una categoría, y SI, puedes tener más de un espacio de nombres dentro de un Rakefile. Ahora si yo hago un "rake --tasks" esto es lo que vería.

  1. rake alcoholic:estarDescerebrado # Esta tarea beberá demasiado
  2. rake alcoholic:mezclarTrago # Esta tarea mezclará un buen cocktail
  3. rake alcoholic:comprarAlcohol # Esta tarea comprará tu Vodka

Por lo tanto ahora para correr estas tareas obviamente correrías "rake alcoholico:estarDescerebrado".


¿Cómo puedo escribir Tareas Ruby útiles?

Bien, solo escribe Ruby! No es broma. Recientemente necesité escribir un script que crease un par de directorios, por lo que terminé escribiendo una tarea Rake que luce algo como esto:

  1. desc "Crea directorios vacíos si no existen"
  2. task(:create_directories) do
  3. # Los directorios que necesito crear
  4. shared_folders = ["icons","images","groups"]
  5. for folder in shared_folders
  6. # Chequea para ver si existen
  7. if File.exists?(folder)
  8. puts "#{folder} existe"
  9. else
  10. puts "#{folder} no existe por lo que se esta creando"
  11. Dir.mkdir "#{folder}"
  12. end
  13. end
  14. end

Por defecto Rake tiene acceso a todo lo que encuentres en File Utils, pero también puedes incluir cualquier extra que quieras como lo harías en Ruby.

¿Cómo escribo Tareas Rake para mi aplicación Rails?

Las aplicaciones Rails vienen con un montón de tareas rake preexistentes, la cuales puedes listar yendo al directorio de tu aplicación y escribir "rake --tasks". Si aún no lo has intentado, hazlo ahora, estaré esperando....

Para crear nuevas tareas rake para tu app Rails, necesitas abrir el directorio /lib/tasks (el cual deberías tener). Si creas tu propio Rakefile en este directorio, y lo denominas "algo.rake", la tarea será levantada automáticamente. Estas tareas serán agregadas a la lista de tareas Rake de la aplicación, y podrás correrlas desde el directorio raíz de la misma. Tomemos el ejemplo de arriba y brindémoselo a nuestra aplicación rails.


utils.rake

  1. namespace :utils do
  2. desc "Crea directorios vacíos si no existen"
  3. task(:create_directories) do
  4. # Los directorios que necesito crear
  5. shared_folders = ["icons","images","groups"]
  6. for folder in shared_folders
  7. # Chequea para ver si existen
  8. if File.exists?("#{RAILS_ROOT}/public/#{folder}")
  9. puts "#{RAILS_ROOT}/public/#{folder} existe"
  10. else
  11. puts "#{RAILS_ROOT}/public/#{folder} no existe por lo que se esta creando"
  12. Dir.mkdir "#{RAILS_ROOT}/public/#{folder}"
  13. end
  14. end
  15. end
  16. end

Note en este snippet como usé #{RAILS_ROOT} para obtener el path completo. SI ahora corro "rake --tasks" en el directorio base de mi aplicación, podré ver a esta nueva función entremezclada con todas las otras tareas rake de Tails:

  1. ...
  2. rake tmp:pids:clear # Clears all files in tmp/pids
  3. rake tmp:sessions:clear # Clears all files in tmp/sessions
  4. rake tmp:sockets:clear # Clears all files in tmp/sockets
  5. rake utils:create_directories # Crea directorios vacíos si no existen
  6. ...

Muy bien! Ahora aquí es donde se vuelve realmente útil..

¿Puedo acceder a mis modelos Rails desde una tarea?

SI, SI, SI!!! De hecho esto es para lo que uso rake mayormente: Escribiendo tareas que necesito correr manualmente en una ocasión, u otras que agendaré para correr automáticamente(usando cronjobs). Como dije al inicio del artículo, uso las taras Rake para las siguientes cosas:

  1. Obtener una lista de miembros para enviarles un email.
  2. Hacer calculos de datos y reportes a última hora.
  3. Expirar y regenerar caches
  4. Hacer respaldos de mi base de datos y del repositorio subversion
  5. Correr cualquie tipo de script de manipulación de datos.

Realmente util, pero además de eso es fácil. Aquí hay una tarea rake que encuentra a los usuarios cuyas suscripciones están por expirar, y les envía un email:

utils.rake

  1. namespace :utils do
  2. desc "Encuentra y envia emails a suscripciones prontas a expirar"
  3. task(:send_expire_soon_emails => :environment) do
  4. # Find users to email
  5. for user in User.members_soon_to_expire
  6. puts "Emailing #{user.name}"
  7. UserNotifier.deliver_expire_soon_notification(user)
  8. end
  9. end
  10. end

Como puedes ver, hay un solo paso para obtener acceso a tus modelos, la cosa "=>:environment"

  1. task(:send_expire_soon_emails => :environment) do

Para correr esta tarea en mi db de desarrollo yo correría "rake utils:send_expire_soon_emails". Si yo quiero correr esto en mi base de datos de producción correría "rake RAILS_ENV=production utils:send_expire_soon_emails".
Si yo entonces quiero correrlo a última hora a medianoche en mi base de datos de producción, podría escribir un cronjob que luciría algo así.

  1.  
  2. 0 0 * * * cd /var/www/apps/rails_app/ && /usr/local/bin/rake RAILS_ENV=production utils:send_expire_soon_emails

Muy conveniente!

¿Dónde puedo encontrar más ejemplos?

Ahora que conoces bastante como para empezar a escribir útiles tareas rake, supongo que te dejarñe con algunos recursos mas. La mejor manera de mejorar tu programación es leer el código de otras personas, por lo que algunos de esos existen porque la gente ha creado tareas rake útiles.

Ya tienes todo junto. Si encuentras otros, siéntete libre de postearlos en los comentarios.

¿Aún leyendo? Si estas ahí, quiero hacerte saber que estamos buscando más gente para escribir para RailsEnvy. Si tienes alguna idea para algún buen tutorial de Rails queremos escucharte!. Básicamente trabajariamos contigo para corregir el tutorial y ayudar en el pulido (actuando como editor). Definitivamente podría ser una buena forma de tener tu nombre ahí fuera, y de comenzar a obtener algunos hits (para tu blog o compañía). Envía un email a Gregg at RailsEnvy si estás interesado.

Continuación

Como continuación, he recibido un email de Jim hace unos pocos minutos, explicando como puedo simplificar mi script de creación de directorios:

  1. # This is needed because the existing version of directory in Rake is slightly broken, but Jim says it'll be fixed in the next version.
  2. alias :original_directory :directory
  3. def directory(dir)
  4. original_directory dir
  5. Rake::Task[dir]
  6. end
  7.  
  8. # Do the directory creation
  9. namespace :utils do
  10. task :create_directories => [
  11. directory('public/icons'),
  12. directory('public/images'),
  13. directory('public/groups'),
  14. ]
  15. end

:)