Wraz z nadchodzącymi wielkimi krokami Rails 3 sporo mówi się o jednym konkretnym gemie. Ma ułatwić zarządzanie zależnościami w aplikacjach. Ma uprościć pracę przy tworzeniu i korzystaniu z gemów. Generalnie ma zbawić świat, uzależnić wszystkich developerów od siebie, a następnie odpalić cap deploy:skynet i po sprawie... OK, może bez tego ostatniego.

Warto jednak wiedzieć, że Bundler nie jest jedynie częścią Rails 3. Jest niezależnym, potężnym narzędziem, które można - i warto - używać przy tworzeniu wszelkich aplikacji w Rubym.

Dlaczego?

Wszystko zaczęło się w 428 r. p.n.e... nie wróć to znowu nie to. Budowa aplikacji, choćby najmniejszych, wymaga od programisty korzystania z wielu narzędzi. Dzięki dobrodziejstwom open-source mamy do dyspozycji tysiące bibliotek napisanych przez tysiące innych developerów. Każda z nich to żyjący własnym życiem twór, który w trakcie rozwoju i działania naszego projektu może przechodzić przez multum zmian. Jak zapewnić, że zmiana w zewnętrznym module nie wpłynie negatywnie na działanie naszej aplikacji? Jak zadbać o to, by każdy developer w zespole miał identyczne środowisko? Railsy radziły sobie do tej pory korzystając z wpisów config.gem w pliku environment.rb - możliwość określenia specyficznej wersji i źródła gema daje pewne zabezpieczenie, choć mocno ogranicza.

Ale świat nie kończy się na Rails, rozbudowane zależności mogą mieć wszystkie aplikacje w Rubym. Co więcej, same gemy zależą nierzadko od innych gemów i tu zaczynają się poważne schody. Developerzy wynajdowali patenty na zapewnienie zgodności, m.in. hardkodując je w źródle swoich bibliotek. Takie potworki zamiast sprawę rozwiązywać, tylko ją komplikowały.

Bundler ma na celu wszystkie te problemy rozwiązać.

Jak?

Weźmy na warsztat prosty skrypt z jedną zależnością:

require "rubygems"
require "nokogiri"
require "open-uri"

doc = Nokogiri::HTML(open('http://www.google.com/search?q=lukasz+adamczak'))
link = doc.css('h3.r a.l').first
puts link.content

Jeśli w systemie mamy nokogiri, program powinien wypisać:

lans musi być

Jeśli nie, przeczytamy całkiem słuszny komunikat:

no such file to load -- nokogiri

Idąc tradycyjną ścieżką, moglibyśmy teraz zainstalować gem install nokogiri i problem rozwiązany. Pojdźmy jednak The Bundler Way. Instalujemy bundlera (na dzień dzisiejszy aktualna wersja to 1.0.0.rc.2, więc trzeba użyć --pre)

gem install bundler --pre

Otrzymujemy bardzo potężne polecenie bundle, którego możemy użyć już teraz. bundle init utworzy w bieżącym katalogu prosty wzorcowy plik Gemfile, który będzie opisywał wszystkie zależności naszego projektu. Drugi krok to użycie Bundlera w kodzie. Zmieniamy układ require'ów na:

require "rubygems"
require "bundler/setup"
require "nokogiri"
require "open-uri"

require "bundler/setup" gwarantuje, że projekt będzie wykorzystywał do zarządzania zależnościami właśnie bundlera. W tym momencie cały projekt stał się odizolowaną "bańką". Całe środowisko jest opisane przez Gemfile. Nie mają znaczenia gemy jakie masz w systemie. Tylko to, co wyraźnie zaznaczysz w Gemfile'u będzie należało do projektu. Aby się przekonać zainstaluj:

sudo gem install nokogiri

Mogłoby się wydawać, że teraz skrypt wykona się poprawnie. Błąd! Bundler sprawdzi plik Gemfile (domyślnie pusty) i uzna, że nie wie co to nokogiri:

no such file to load -- nokogiri (LoadError)

Dodaj więc do Gemfile linię:

gem "nokogiri"

Teraz skrypt wykonuje się poprawnie. Standardowa kolejność pracy z Bundlerem powinna jednak być odwrotna. Po dodaniu lub zmianie czegokolwiek w Gemfile, pamiętaj o odpaleniu polecenia bundle install. Zajmie się ono instalacją brakujących gemów, w razie potrzeby prosząc nawet o hasło do sudo. Zainstalowane w ten sposób gemy trafiają domyślnie w to samo miejsce gdzie instalowane przez gem install.

bundle ma też kilka innych ciekawych opcji. check do sprawdzenia czy posiadamy wszystko co wymagane.

$ bundle check
Could not find gem 'nokogiri (= 1.4.3.1, runtime)' in any of the gem sources.

(a po instalacji)

$ bundle check
The Gemfile's dependencies are satisfied

console uruchamia irb z wczytanym kompletnym środowiskiem:

$ bundle console
ruby-1.8.7-p249 > Nokogiri
 => Nokogiri

bundle exec umożliwia uruchomienie wykonywalnego skryptu z gema - z tej konkretnej wersji, którą podaliśmy w Gemfile:

$ bundle exec nokogiri http://rubysfera.pl
Your document is stored in @doc...
ruby-1.8.7-p249 >

show i open pozwalają znaleźć i otworzyć katalog ze źródłem gema:

$ bundle show
Gems included by the bundle:
  * bundler (1.0.0.rc.2)
  * nokogiri (1.4.1)

$ bundle show nokogiri
/Users/Czak/.rvm/gems/ruby-1.8.7-p249/gems/nokogiri-1.4.1

$ bundle open nokogiri  # otwiera powyższy katalog w domyślnym edytorze

Aha!

Bundler podczas pracy tworzy automatycznie plik Gemfile.lock na bazie Gemfile oraz stanu Twojego aktualnego środowiska. Jeśli w Gemfile nie określisz wymaganej wersji nokogiri, a w systemie masz zainstalowaną 1.4.1, wtedy do Gemfile.lock trafi:

GEM
  remote: http://rubygems.org/
  specs:
    nokogiri (1.4.1)

Należy pamiętać aby Gemfile.lock dodać do repozytorium wraz z źródłowym Gemfile. To zagwarantuje, że cały zespół będzie pracował na identycznym środowisku. Jeśli w którymś momencie wymusisz w Gemfile wersję:

gem "nokogiri", "1.4.3.1"

wtedy Gemfile.lock zostanie uaktualniony automatycznie przez bundlera i obie zmiany należy wcommitować. Po każdej zmianie w Gemfile (a właściwie jak często się da, np. po git pull) warto uruchamiać bundle install aby zsynchronizować oba pliki i - w razie potrzeby - uaktualnić lokalne środowisko.

Na koniec

Takim krótkim tekstem ledwo musnąłem powierzchnię Bundlera. Może jednak przekonałem kogoś, że jest to narzędzie warte uwagi i jednocześnie łatwe w użyciu. Warto już teraz wyrobić sobie nawyk wywoływania bundle init zaraz po git init, niezależnie od rodzaju budowanej aplikacji. Bundler może w niedługim czasie wprowadzić trochę ładu do Rubinowego światka, na czym skorzysta każdy developer.

Aby dowiedzieć się więcej: