Tutorial Ruby on Rails Rake (czyli jak Rake doprowadził mnie do alkoholizmu)
Wysłane przez Krzysztof Kempiński dnia 02.09.2007
Jako programista Ruby on Rails być może miałeś okazję używać polecenia "rake" do uruchamiania testów, lub "rake db:migrate" by wykonać migrację bazy. Ale czy wiesz co tak naprawdę dzieje się w tych poleceniach? Czy wiesz, że możesz sam tworzyć takie polecenia, nazywane taskami, a nawet budować biblioteki użytecznych plików Rake?
Oto przykłady, jak ja wykorzystuję zadania Rake:
- Wysyłanie e-maili do grupy użytkowników.
- Nocne operacje na danych oraz raportowanie.
- Czyszczenie i odnawianie cache.
- Wykonywanie kopii bazy i backupów repozytoriów.
- Uruchamianie skryptów operujących na danych.
W tym artykule zobaczymy jak Rake został stworzony, i jak może nam pomóc w aplikacjach Railsowych. Po jego przeczytaniu, powinieneś umieć napisać swój własny task i powinieneś wiedzieć jak się upić w trzech krokach.
Zawartość
- Dlaczego make, retrospekcja.
- Jak powstał Rake?
- Jak działa Rake?
- Jak działają zależności w Rake?
- Jak mogę dokumentować moje taski w Rake?
- Rake Namespaces
- Jak pisać użyteczne Rake taski?
- Jak pisać Rake taski współpracujące z aplikację Ruby on Rails?
- Czy mogę korzystać z modeli z Rails w moich Rake taskach?
Dlaczego make, retrospekcja.
Żeby zrozumieć jak powstał Rake, należy się najpierw przyjrzeć starszemu, przodkowi jakim jest Make.
Powróćmy do czasów, kiedy każdy kawałek kodu musiał być skompilowany przed uruchomieniem, zanim jeszcze języki interpretowane i iPhone'y opanowały świat.

Wtedy to program ściągało się jako czysty kod oraz skrypty shellowe, które umożliwiały skompliowanie/zlinkowanie/zbudowanie aplikacji. Wydawało się polecenie run "install_me.sh" (shell script), każda linia była kompilowana, a gdy cały proces się powiódł, mieliśmy gotową do użycia, wykonywalną wersję aplikacji.
Wtedy nie było to niczym uciążliwym dla zwykłych odbiorców oprogramowania. Jednak dla ich developerów był to koszmar. Za każdym razem, nawet po małej zmianie kodu, kiedy chciało się przetestować poprawkę, należało ponownie uruchomić skrypty, które budowały aplikację. Dla większych programów było to bardzo czasochłonne.
W 1977 Stuart Feldman z Bell Labs i wynalazł "Make", który rozwiązał problem tych długotrwałych kompilacji. Make generalnie służy do kompilowania programów, jednak posiada dwie wielkie zalety:
- Make potrafi rozpoznać, które pliki uległy zmianie od ostatniej kompilacji. Wykorzystując tę wiedzę, Make kompiluje tylko te fragmenty (pliki), które uległy zmianie. Pozwala to w bardzo znaczący sposób zredukować czas rekompilacji.
- Make także posiada mechanizm śledzenia zależności, zatem można mu powiedzieć, że plik A wymaga pliku B do kompilacji, a plik B pliku C. Zatem jeśli make chce skompilować plik A, a nie skompilowany jest jeszcze plik B, to najpierw make skompiluje plik B.
Make jest zwykłym programem wykonywalnym, tak jak "ls" czy "dir". Aby make wiedział, jak skompilować aplikację, trzeba utworzyć plik "makefile" z odniesieniami do wszystkich plików, oraz ich zależnościami. Pliki "makefile" mają swoją własną składnię.
Przez lata Make ewoluował i także inne języki zaczęły go używać. Wielu programistów Ruby używało go przed pojawianiem sie Rake.
Zapytasz: "Ale przecież Ruby nie jest językiem kompilowalnym, zatem po co mu takie narzędzie?"
Są dwa powody:
- Tworzenie zadań (tasków) - pracując nad każdą większą aplikacją, dochodzimy do momentu, kiedy trzeba napisać parę skryptów pomocniczych. Np. czyszczenie cache, taski zarządzające, migracje itd. Zamiast tworzyć kilka tego typu skryptów, można je umieścić w pliku Makefile. Taski mogą być wtedy uruchamiane np. tak: make costam (uruchomienie tasku costam).
- Śledzenie zależności zadań - Gdy zaczynasz pisać bibliotekę tasków, stwierdzasz, że część zadań ma wspólne części. Np. zadania "migrate" oraz "schema:dump" wymagają najpierw połączenia z bazą danych. Możesz zatem stworzyć task "connect_to_database" i ustawić, żeby taski "migrate" i "schema:dump" wymagały "connect_to_database". Wtedy, gdy uruchomisz "migrate", "connect_to_database" będzie wykonane najpierw.
Jak powstał Rake?
Kilka lat temu Jim Weirich pracował nad projektem w Javie, używając Make. Pracując nad plikami Makefile, zauważył, jak użyteczne byłoby, gdyby mógł używać w tych plikach małych funkcji w Ruby. Stworzył więc rake.
Jak działa Rake?
Powiedzmy, że chciałbym się upić. Jakie kroki muszę podjąć?
- Kupić alkohol
- Pić alkohol
- Osiągnąć stan upojenia
Jeśli chciałbym użyć Rake, by wykonać te kroki, mogę utworzyć plik "Rakefile":
task :kupicAlkohol do puts "Kupić alkohol" end task :picAlkohol do puts "Pić alkohol" end task :osiagnacUpojenie do puts "Dlaczego wszystko wiruje?" end
Mogę zatem uruchomić taski z tego samego katalogu, w którym jest plik Rakefile:
Kupić alkohol
$ rake picAlkohol
Pić alkohol
$ rake osiagnacUpojenie
Dlaczego wszystko wiruje?
Nieźle! Jednak zadania te mogę uruchamiać w dowolnej kolejności. A przecież nie mogę osiągnąć stanu upojenia przed piciem.
Jak działają zależności w Rake?
task :kupicAlkohol do puts "Kupić alkohol" end task :picAlkohol => :kupicAlkohol do puts "Pić alkohol" end task :osiagnacUpojenie => :picAlkohol do puts "Dlaczego wszystko wiruje?" end
Kupić alkohol
$ rake picAlkohol
Kupić alkohol
Pić alkohol
$ rake osiagnacUpojenie
Kupić alkohol
Pić alkohol
Dlaczego wszystko wiruje?
Rake taski, które do tej pory zbudowaliśmy są stosunkowo proste, a ich nazwy doskonale opisują zadanie. Ale w prawdziwym projekcie informatycznym może się okazać konieczne dokumentowanie tasków.
Jak mogę dokumentować moje taski w Rake?
Rake udostępnia bardzo prostą funkcję do dokumentowania - "desc":
desc "Task umożliwiający nabycie alkoholu" task :kupicAlkohol do puts "Kupić alkohol" end
Po wpisaniu w konsoli "rake -T", lub "rake --tasks" otrzymamy między innymi:
rake kupicAlkohol # Task umożliwiający nabycie alkoholu
Rake Namespaces
Namespeces umożliwiają budowanie lepszej hierarchii tasków. Powyższy przykład mógłby wyglądać tak:
namespace :alkoholik do desc "Task umożliwiający nabycie alkoholu" task :kupicAlkohol do puts "Kupić alkohol" end desc "Task umożliwiający picie alkoholu" task :picAlkohol => :kupicAlkohol do puts "Pić alkohol" end desc "Task umożliwiający osiągnięcie stanu upojenia" task :osiagnacUpojenie => :picAlkohol do puts "Dlaczego wszystko wiruje?" end end
Tak więc teraz najszybszym sposobem na upicie jest "rake alkoholik:osiagniecieUpojenia"
Jak pisać użyteczne Rake taski?
W Rake taskach możliwe jest pisanie kodu w Ruby. Przykładowy kod taska, który tworzy kilka katalogów, mógłby wyglądać tak:
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
Domyślnie rake taski mają dostęp do wszystkiego z klasy File Utils, ale można oczywiście dołączyć dowolny kod.
Jak pisać taski współpracujące z aplikacją Ruby on Rails?
Rails'y są rozpowszechniane z wieloma użytecznymi taskami, które możesz wylistować komendą "rake --tasks". Aby stworzyć swoje rake taski współpracujące z Railsami, wejdź do katalogu /lib/tasks. Tutaj stwórz plik, nazywając go "przyklad.rake". Następnie wklej do pliku poniższy przykład taska współpracującego z Railsami. Od razu będzie on gotowy do użycia, poprzez uruchomienie z głównego katalogu aplikacji, komendy: "rake utils:create_directories"
utils.tasks
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
Czy mogę korzystać z modeli z Rails w moich rake taskach?
Oczywiście. Jest to chyba najczęściej wykorzystywana funkcjonalność. Weźmy przykład wysyłania maili do userów:
utils.tasks
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
Zatem, jak widać, wszystko czego potrzebujemy, to "=> :environment" za nazwą taska. Żeby uruchomić nasz task, wystarczy wpisać "rake utils:send_expire_soon_emails" w konsoli. Jeśli chcemy wymusić użycie taska w danym środowisku, np. produkcyjnym, wystarczy uruchomić: "rake RAILS_ENV=production utils:send_expire_soon_emails"
Poprzez odpowiednie wpisy do crontab, możemy sprawić, by maile były wysyłane cyklicznie.
Artykuł jest tłumaczeniem tekstu który ukazał się w serwisie railsenvy.com
Drobne poprawki i kosmetyka artykułu: Mateusz Borowiak.
