Руководство по Rake (перевод)

Оригинал расположен здесь. Не забудте туда заглянуть, тем есть забавные картинки и фотографии авторов блога.
Автор статьи - Gregg Pollack.

Перевод получился скорее вольно-художественный, нежели дословный.
Вставки от переводчика, то бишь от меня по тексту выделены курсивом.
Собственно дальше пошел уже сам перевод:

Как Rails-разработчик, вы наверняка использовали “rake” при тестировании или запускали “rake db:migrate” для выполнения миграций. Но понимаете ли вы, что происходит внутри этих Rake tasks? Вы в курсе, что можно написать свои задания или даже создать библиотеку полезных Rake-файлов?

Вот несколько примеров того, для чего я использую Rake tasks:

Содержание

  1. Почему make? Экскурс в историю.
  2. Откуда у нас появился Rake?
  3. Как Rake работает?
  4. Как использовать зависимости в Rake?
  5. Как документируются Rake-задания?
  6. Пространства имен в Rake
  7. Как написать свои задания на Ruby?
  8. Как написать написать свои Rake-задания для моего Rails-приложения?
  9. Могу я получить доступ к своим Rails-моделям из Rake-задания?
  10. Где еще можно почитать про Rake?

Почему make? Экскурс в историю.

Прежде чем понять, откуда взялся Rake, давайте сначала вспомним про его прадедушку Make.
Давайте мысленно перенесемся в то время, когда каждый кусок кода должен был компилироваться, когда интерпретируемые языки и айФоны еще не покорили Землю.

В то время вы получали программу в виде кучи исходного кода и shell-скрипта. Этот скрипт содержал весь неодходимый код, необходимый вашему компьютеру, чтобы собрать приложение. Вы запускали “install_me.sh” (этот самый shell-скрипт), выполнялась строчка за строчкой (обычно каждая строка – это компиляция одного исходного файла), и в итоге вы получали исполняемый файл, который уже можно было запустить.

Впринципе это подходило большинству людей, за исключением тех, кого угораздило эту программу разрабатывать. Каждый раз даже после незначительного изменения в коде, если вы хотели его проверить, вы должны были снова запускать этот скрипт и перекомпилировать все заново. Очевидно, это могло занимать много времени для больших программ.

В 1977 (в этом году я родился) Stuart Feldman из Bell Labs создал “Make”, которые решал проблему долгой перекомпиляции. Make тоже использовался для компиляции програм, но с двумя существенными отличиями:

  1. Make распознавал какие исходные файлы изменились с момента последней компиляции. Используя эту информацию, при повторной компиляции Make компилировал только изменившиеся исходные файлы. Это дало огромный прирост в скорости перекомпиляции больших програм.
  2. Кроме этого Make содержал в себе отслеживание зависимостей, так что вы могли сказать компилятору, что чтобы нормально скомпилироваться, исходному файу А нужен исходный файл Б, а файлу Б требуется файл В. Таким образом, если Make-у нужно скомпилировать файл А и файл Б еще не скомпилирован, Б компилировался в первую очередь.

Следует также объяснить, что Make – это просто исполняемый файл, такой же как “dir” или “ls”. Для того чтобы объяснить Make-у, как скомпилировать программу,нужно создать “makefile”, который содержал бы все инструкции и зависимости для компиляции “исходников”. У makefile-ов есть свой хитровывернутый синтаксис, который нам здесь знать не обязательно.

Через некоторое время Make эволюционировал и им стали пользоваться во всяких разных языках программирования. Действительно, много Ruby-программистов пользовались им, пока ему на смену не пришел Rake.

“Эээ, но ведь в Ruby ничего не надо компилировать, зачем бы это могло понадобиться Ruby-программистам?”, спросите вы.

Да, Ruby – это интерпретируемый язык, и нам не надо компилировать наш код, так почему программисты на Ruby использовали Make?

Ну, по двум большим причинам:

  1. Создание заданий – В каждом большом приложении почти всегда возникает необходимость в скриптах, которые можно было бы запускать из командной строки. Вместо создания десяти отдельных скриптов (или одного универсального), вы можете создать один “Makefile”, в котором все можно разделить на задания. После этого их можно выполнять, просто набирая в командной строке “make stupid”, где stupid – это название вашего задания.
  2. Отслеживание зависимостей между заданиями – Когда вы начинаете писать библиотеку для повседневных задач, то сталкиваетесь с тем, что некоторые задачи частично повторяют друг друга. Например, оба задания “migrate” и “schema:dump” требуют соединения с базой данных. Я мог бы создать задание “connect_to_database”, и установить, что “migrate” и “schema:dump” зависят от него. Таким образом, в следующий раз, когда я запущу “migrate”, перед ним автоматически запустится “connect_to_database”

Откуда у нас появился Rake?

Несколько лет назад Jim Weirich работал над Java-проектом, в котором он использовал Make. Во время написания Makefile он подумал, что было бы удобно писать небольшие куски кода на Ruby прямо внутри мэйкфайлов. Ну и короче он написал rake.
Тут я малость опустил про Джима Вейриха, кому интересно загляните в оригинал, там и фотка есть (Jim Weirich в полосатой футболке :)

Ну и как rake уже в конце концов работает?

Допустим, мы хотим напиться, какие шаги нам нужно предпринять?

  1. Купить водки (purchaseAlchohol)
  2. Замешать коктейль (mixDrink)
  3. Нажратсо (getSmashed)

Если бы я захотел использовать Rake для вызова таких задач, я бы создал Rake-файл с примерно таким содержимым:


task :purchaseAlchohol do
  puts "Купил водки"
end        

task :mixDrink do
  puts "Замешал коктейль 'Мохнатый пупок'"
end        

task :getSmashed do
  puts "Ччувак, чота ты какой-то размытый..И-и-ик, накатим еще по стопарю?"
end

После этого я могу исполнять эти задания(находясь в той же директории), что-то типа такого:


$ rake purchaseAlchohol
Купил водки        

$ rake mixDrink
Замешал коктейль 'Мохнатый пупок'        

$ rake getSmashed
Ччувак, чота ты какой-то размытый.. И-и-ик, накатим еще по стопарю?

Круто, да? Тем не менее с точки зрения зависимостей, я могу выполнять эти задания в любом порядке. Но вообще говоря, если бы я пожелал “нажратсо” перед “замешать коктейль” или “купить водки”, то обычно это находится за пределами человеческих возможностей (это не про русских, да – прим. переводчика)

Итак, как мне использовать зависимости в Rake?


task :purchaseAlchohol do
  puts "Купил водки"
end        

task :mixDrink => :purchaseAlchohol do
  puts "Замешал коктейль 'Мохнатый пупок'"
end        

task :getSmashed => :mixDrink do
  puts "Ччувак, чота ты какой-то размытый.. И-и-ик, накатим еще по стопарю?"
end

Итак, теперь я говорю, что если я хочу “замешать коктейль”, то надо бы сначала “купить водки”, а если хочу “нажратсо”, то сначала надо “замешать коктейль”. Как вы уже наверное догадались, в итоге получаем что-то такое:


$ rake purchaseAlchohol
 Купил водки        

$ rake mixDrink
 Купил водки
 Замешал коктейль 'Мохнатый пупок'        

$ rake getSmashed
 Купил водки
 Замешал коктейль 'Мохнатый пупок'
 Ччувак, чота ты какой-то размытый.. И-и-ик, накатим еще по стопарю?

Как видно, теперь при попытке “Нажратсо”, вызываются зависимые задания “купить водки” и “замешать коктейль”.

Через какое-то время вам возможно захочется усугубить ваши пристрастия и расширить ваш Rakefile. И даже может быть захочется привлечь к этому своих друзей. Сделаем вид, что мы как будто бы в настоещем софтверном проекте, соответственно если мы хотим добавить людей в команду, то надо убедиться что все хорошо задокументировано. Итак, закономерный следующий пункт:

Как документируются Rake-задания?

Собственно в Rake с этим все очень просто. Вызываем desc и вперед:


desc "Это задание купит нам водки"
task :purchaseAlchohol do
  puts "Купил водки"
end        

desc "Это задание замешает нам отличный коктейль"
task :mixDrink => :purchaseAlchohol do
  puts "Замешал коктейль 'Мохнатый пупок'"
end        

desc "Это задание позволит нам как следует нажраться"
task :getSmashed => :mixDrink do
  puts "Ччувак, чота ты какой-то размытый.. И-и-ик, накатим еще по стопарю?"
end

Ну вот, теперь у наших заданий есть описания. Теперь я или мои друзья могут вызвать команду "rake -T" или "rake --tasks"


$rake --tasks        

 rake getSmashed        # Это задание позволит нам как следует нажраться
 rake mixDrink          # Это задание замешает нам отличный коктейль
 rake purchaseAlcohol  # Это задание купит нам водки

Ну несложно, да?

Пространства имен в Rake

Ну раз уж мы окончательно пристрастились к алкоголю, мы теперь используем множество разных Rake-заданий, и нам не помешало бы их категоризировать. Для этого используем пространства имен. Для нашего примера это будет выглядеть так:


namespace :alcoholic do
desc "Это задание купит нам водки"
  task :purchaseAlchohol do
    puts "Купил водки"
  end        

  desc "Это задание замешает нам отличный коктейль"
  task :mixDrink => :purchaseAlchohol do
    puts "Замешал коктейль 'Мохнатый пупок'"
  end        

  desc "Это задание позволит нам как следует нажраться"
  task :getSmashed => :mixDrink do
    puts "Ччувак, чота ты какой-то размытый.. И-и-ик, накатим еще по стопарю?"
  end
end

Пространства имен позволяют нам группировать задания по категориям и, О ДА, может быть более одного пространство имен в Rakefile-е. Если теперь запустить "rake --tasks", то вот что мы увидим:


$rake --tasks        

 rake alcoholic:getSmashed        # Это задание позволит нам как следует нажраться
 rake alcoholic:mixDrink          # Это задание замешает нам отличный коктейль
 rake alcoholic:purchaseAlcohol  # Это задание купит нам водки

Ну и чтобы вызвать наши задания, очевидно нужно выполнить команду “rake alcoholic:getSmashed”.

Как мне написать свои задания на Ruby?

Ну собственно берем и пишем на Ruby. Без шуток. Недавно мне понадобился скрипт, который создавал несколько дерикторий, в общем законченное задание выглядит так:


desc "Create blank directories if they don't already exist"
task(:create_directories) do        

  # The folders I need to create
  shared_folders = ["icons","images","groups"]        

  for folder in shared_folders        

    # Check to see if it exists
    if File.exists?(folder)
      puts "#{folder} exists"
    else
      puts "#{folder} doesn't exist so we're creating"
      Dir.mkdir "#{folder}"
    end        

  end
end

По умолчанию, rake имеет доступ ко всему, что есть в File Utils, но ничто не мешает вам подключить все что душе угодно, лишь бы оно было на Ruby.

Как мне написать написать свои Rake-задания для моего Rails-приложения?

В Rails уже имеется некоторое количество готовых rake заданий, которые можно просмотреть с помощью "rake --tasks". Если вы еще этого не сделали, тогда сделайте, я подожду…

Чтобы создать новые задания для вашего рельсового приложения, вам надо открыть директорию /lib/tasks (что вы уже должны были сделать). Если в этой директории вы создадите свой Rakefile “something.rake”, соответствующие задания будут автоматически подгружены. Они будут добавлены в список существующих заданий и вы можете запускать их обычным способом. Вот так будет выглядеть предыдущий пример в Rails-окружении:


namespace :utils do
  desc "Create blank directories if they don't already exist"
  task(:create_directories) do        

    # The folders I need to create
    shared_folders = ["icons","images","groups"]        

    for folder in shared_folders        

      # Check to see if it exists
      if File.exists?("#{RAILS_ROOT}/public/#{folder}")
        puts "#{RAILS_ROOT}/public/#{folder} exists"
      else
        puts "#{RAILS_ROOT}/public/#{folder} doesn't exist so we're creating"
        Dir.mkdir "#{RAILS_ROOT}/public/#{folder}"
      end        

    end
  end
end

Заметьте, что здесь мы можем использовать #{RAILS_ROOT}, чтобы получить полный путь. После добавления такого rake-файла, "rake --tasks" выдаст нам следующее:


...
rake tmp:pids:clear              # Clears all files in tmp/pids
rake tmp:sessions:clear          # Clears all files in tmp/sessions
rake tmp:sockets:clear           # Clears all files in tmp/sockets
rake utils:create_directories    # Create blank directories if they don't already exist
...

Замечательно, теперь можно рассмотреть пример, где это просто суперполезно..

Могу я получить доступ к своим Rails-моделям из Rake-задания?

Всенепременно! Собственно, для этого по большей части я и использую Rake: Пишу задания, которые необходимо вызывать периодически вручную или автоматически по крону. Как я уже сказал в начале статьи, я использую Rake для следующих задач:

Обалденно удобно, и легко. Вот rake задание, которое находит пользователей, у которых истекает срок подписки и рассылает им уведомления:


require File.expand_path(File.dirname(__FILE__) + "/../../config/environment")        

namespace :utils do
  desc "Finds soon to expire subscriptions and emails users"
  task(:send_expire_soon_emails => :environment) do
        # Find users to email
        for user in User.members_soon_to_expire
                puts "Emailing #{user.name}"
                UserNotifier.deliver_expire_soon_notification(user)
        end
  end
end

Как видно, доступ к нашим моделям производится с помощью "require …" и "=> :environment":

  1. require File.expand_path(File.dirname(FILE) + ”/../../config/environment”)
  2. task(:send_expire_soon_emails => :environment) do

Запускаем задание на development-базе даных: “rake utils:send_expire_soon_emails”
На “боевой” базе данных: “rake RAILS_ENV=production utils:send_expire_soon_emails”

Чтобы задание выполнялось по ночам, можно добавить в cron следующее:


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

Вот!

Где еще можно почитать про Rake?

Теперь, когда вы уже знаете достаточно, чтобы начать писать свои суперполезные rake-задания, можно дать вам несколько ссылок по теме. Лучший способ улучшить свои программистские навыки – это чтение чужого кода, несколько следующих ссылок – это как раз наборы готовых rake tasks.

В догонку

Тут вот товарищ Джим прислал письмо с объяснением, как можно упростить мой скрипт для создания директорий:


# 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.
alias :original_directory :directory
def directory(dir)
  original_directory dir
  Rake::Task[dir]
end        

# Do the directory creation
namespace :utils do
  task :create_directories => [
    directory('public/icons'),
    directory('public/images'),
    directory('public/groups'),
  ]
end

Об авторе

Автора зовут Немытченко Иван.
Он занимается разработкой веб-приложений.
Сейчас активно использует в работе Ruby и Rails.
Здесь делится своими
мыслями по этой и смежным темам.
Дневник общего назначения: zoob.ru.

Рубрики