Ostatnio w elixir mieliśmy styczność z projektem typu ubmbrella, który umożliwiał nam tworzenie rozwiązań wieloprojektowych. W tym także zarządzania zależnościami pomiędzy tymi projektami. Dziś właśnie na zależnościach chcę się skoncentrować.
Zanim przejdziemy do konkretów, warto wspomnieć o tym, że pomiędzy elixir 1.3 a 1.4 nastąpiła pewna znacząca zmiana, znacząco ułatwiająca nam operowanie na zależnościach. Kiedyś musieliśmy je wprowadzać w dwóch miejscach, od 1.4 w jednym miejscu. Wszystkie nasze zależności trafiają do metody deps/0
w mix.exs
:
defp deps do [] end
Która zwraca wszystkie zależności jakie dany projekt potrzebuje by działać poprawnie. Wszystkie te zależności zostaną uruchomione/zainicjalizowane przed zainicjalizowaniem naszej aplikacji. Jest to domyślne zachowanie od 1.4.
Side Note
Wcześniejszych wersjach, trzeba jeszcze podać w metodzie application/0
:
def application do [applications: [:logger, :inne, :zaleznosci]] end
To często z tego co czytałem i sam się mogę domyślić stanowiło niezły problem – ludzie zapominali o tym, nie dodawali, gubili się itp. Itd.. Aktualnie application/0
wygląda tak:
def application do # Specify extra applications you'll use from Erlang/Elixir [extra_applications: [:logger]] end
Zamiast podawać wszystkie aplikację, podajemy tylko te które są dostępne w środowisku i nie są zależnościami.
Co to jest zależność?
W Elixir zależność to aplikacja zewnętrzna, która nie jest częścią naszego kodu danego projektu. Czyli jeżeli stworzymy dwa projekty za pomocą mix
– t1
i t2
. to t1
może wymagać zależności do t2
. Na przykład taki t2
to będzie generyczny serwer chat, który możemy wykorzystywać w wielu projektach. Zaś t1
to będzie aplikacja dla klienta, który wymaga chatu.
W Elixir różnica pomiędzy takim .NET jest taka, że każda z paczek które się ściągnie musi zostać skompilowana. Bez tego jest ona bezużyteczna.
Zależności możemy podzielić na dwa główne typy: wewnętrzne i zewnętrzne. Wewnętrzne to takie, które mają referencje projektów które powinny być prywatne lub takie, których firma nie chce udostępniać publicznie. Jest chyba w 95% przypadków. Zewnętrzne zaś to takie, które znajdują się w miejsca publicznych – na przykład git lub jakiś package manager (o czym zaraz).
Wszystkie zależności zaś instaluje się w ten sam zunifikowany sposób o czym więcej poniżej.
Jakiego rodzaju mamy zależności?
Dostępne są 3 rodzaje zależności
- Zależność znajdująca się pod określoną ścieżką (w tym i w umbrella)
- Zależność jako repozytoria git.
- Zależności hostowane w sieci na stronie package manager – hex.
Zależność zaś deklaruje się jako tuple w którym pierwszy parametr to nazwa zależności jako atom
(nazwa zależności == nazwa aplikacji), drugi już określa opcje i/lub rodzaj zależności. Taki tuple dodajemy do listy w deps/0
w pliku mix.exs
.
W opcjach, możemy ustawić takie rzeczy jak komendę w przez jaką zależność będzie kompilowana (:compile
), czy zależność jest opcjonalna (:optional
) czy nawet dla jakiego środowiska ona powinna być pobrana/zainstalowana (:only
). No i parametr :runtime
, dzięki któremu nie musimy dodawać naszej zależności do listy aplikacji w application/0
. Więcej opcji znajdziecie w dokumentacji.
Dla przykładu, deklaracja zależności może wyglądać następująco (wszystko zostanie omówione poniżej):
{:plug, "~> 1.3"} {:t1, git: "https://github.com/gutek/t1/t1.git", tag: "0.1.0"}
Path
Zależność znajduje się pod określoną ścieżką:
{:t1, path: "proj/src" }
Szczególnym przypadkiem jest typ aplikacji umbrella, w którym zamiast path możemy podać in_umbrella
co domyślnie ustali ścieżkę na ../[app]
.
{:t2, in_umbrella: true }
Git
Git daje nam proste opcje do określenia z jakiego repro i jaką wersję mamy pobierać danej zależności. Przy czym możemy określić tag
czy branch
oraz czy mamy pobierać submodules
.
To co oferuje git jeszcze to skrót do github, jeżeli kod znajduje się na github to nie musimy pisać github.com wystarczy zamiast git
uzyć github
.
Przykłady:
{:t1, git: "https://github.com/gutek/t1/t1.git", tag: "0.1.0"} {:t1, github: "gutek/t1/t1.git", branch: "dev"} {:t1, github: "gutek/t1/t1.git", branch: "alpha", submodules: true}
Hex
Hex, to package manager dla elixira i erlanga. Ogólnie zawiera on listę dostępnych paczek z pewnymi statystykami.
Instalacja i zarządzanie jest dla nas przezroczyste. Cały czas się zastanawiam, czy nie powinno być osobnego postu na temat hex, ale zobaczymy. Bo tak naprawdę nie ma więcej co dodać do tego co już napisałem :)
Jeżeli nasza zależność jest zdefiniowana w ten sposób:
{:plug, "~> 1.3"}
Mix przy instalacji paczki automatycznie sięgnie po paczkę na hex. Zaś magiczny string "~> 1.3"
jest numerem wersji jaki nas interesuje. Numer wersji zaś jest zgodny z schematem SchemVer 2.0. Zaś "~"
tylda jest to prosty DSL który rozwijany jest jako numer wersji który jest pomiędzy tą zdefiniowaną a odpowiednią wyższą. Dla przykładu ~> 2.0
znaczy, że wszystko pomiędzy wersjami 2.0
a < 3.0
. Zaś ~> 2.1
, wszystko pomiędzy 2.1.1
a < 2.2
.
Z ~
nie musimy korzystać, możemy zapisać ograniczenia zgodnie z schematem SchemVer.
Jak się instaluje/aktualizuje/kompiluje zależności?
Instalowanie zależności jest w elixir zunifikowane i wszystko jest dostępne poprzez aplikację mix
z parametrem deps
.
Dla przykładu będę się posługiwał 2 zależnościami:
defp deps do [{:plug, "~> 1.3"}, {:cowboy, "~> 1.1"}] end
Dla scenariusza powyżej, sytuacja jest taka, że paczka plug
była już sobie ściągnięta, zaś cowboy
jest nowy.
W celu sprawdzenia jakie zależności są zdefiniowane i które z nich są zainstalowane wystarczy komenda:
$ mix deps * cowboy (Hex package) the dependency is not available, run "mix deps.get" * mime (Hex package) (mix) locked at 1.0.1 (mime) 05c39385 the dependency build is outdated, please run "mix deps.compile" * plug (Hex package) (mix) locked at 1.3.0 (plug) 6e2b01af the dependency build is outdated, please run "mix deps.compile"
W przykładzie powyżej jest powiedziane, że mam dwie paczki ściągnięte (plug
i mime
, mimie
jest paczką wymaganą przez plug
), zaś cowboy
jest nieściągnięty. Przy każdej z paczce jest informacja co mamy zrobić dalej.
Ale nie tylko mix.deps
da nam taką podpowiedź. Także iex -S mix
:
Unchecked dependencies for environment dev: * cowboy (Hex package) the dependency is not available, run "mix deps.get" ** (Mix) Can't continue due to errors on dependencies
Więc teraz wystarczy:
mix deps.get
By sięgnąć brakującą paczkę (oraz wszystkie wymagane paczki przez cowboy
). Teraz mamy już dwie drogi, albo wykonamy mix deps.compile
albo iex -S mix
. Obydwie komendy wykonają kompilację naszych zależności.
$ iex -S mix Erlang/OTP 19 [erts-8.0.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace] ===> Compiling ranch ===> Compiling cowlib src/cow_multipart.erl:392: Warning: crypto:rand_bytes/1 is deprecated and will be removed in a future release; use crypto:strong_rand_bytes/1 ===> Compiling cowboy ==> mime Compiling 1 file (.ex) Generated mime app ==> plug Compiling 44 files (.ex) Generated plug app ==> t1 Compiling 1 file (.ex) Generated t1 app Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>
Po skompilowaniu paczek, pojawia się nam plik mix.lock
, który zawiera informację o wersjach i paczkach zainstalowanych. Ten plik powinien trafić do repo, gdyż na podstawie niego, wszyscy dev będą mieli takie same wersje bibliotek – tak jakby robi ograniczenie do konkretnej wersji:
%{"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []}, "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []}, "plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []}}
Czasami jednak jakieś wersje mogą znikać z hex, w tym wypadku możemy wykonać operację:
mix deps.unlock
To umożliwi hexowi na ściągnięcie innych wersji niż te w lock file.
Aktualizacja paczek następuje poprzez:
mix deps.update
Co także aktualizuje plik lock.
Ciekawą komendą na koniec jest komenda deps.tree
:
$ mix deps.tree t1 ├── cowboy ~> 1.1 (Hex package) │ ├── cowlib ~> 1.0.2 (Hex package) │ └── ranch ~> 1.3.2 (Hex package) └── plug ~> 1.3 (Hex package) ├── cowboy ~> 1.0.1 or ~> 1.1 (Hex package) └── mime ~> 1.0 (Hex package)
Która wizualnie pokaże nam zależności naszego projektu i bibliotek elixirowych które są zainstalowane.
To samo, tylko na poziome aplikacji i wraz z bibliotekami erlangowymi robi:
$ mix app.tree: t1 ├── elixir ├── logger │ └── elixir ├── cowboy │ ├── ranch │ │ └── ssl │ │ ├── crypto │ │ └── public_key │ │ ├── asn1 │ │ └── crypto │ ├── cowlib │ │ └── crypto │ └── crypto └── plug ├── elixir ├── crypto ├── logger └── mime └── elixir
Podsumowanie
Zależności w elixir nie są niczym skomplikowanym i jeżeli mieliśmy styczność z zależnościami w innych środowiskach to tutaj problemu nie będziemy mieli. To co jest fajne, to, że mix deps działa dla nas przezroczyście – nie interesuje nas czy to jest github, package manager czy ścieżka. mix deps
się tym zajmie.
Do tego jeżeli o czymś zapomnimy, mix postara się byśmy się o tym dowiedzieli :)