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ą mixt1 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

  1. Zależność znajdująca się pod określoną ścieżką (w tym i w umbrella)
  2. Zależność jako repozytoria git.
  3. 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.

HEX - Package Manager for Elixir
HEX – Package Manager for Elixir

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 :)