依存関係

この記事は英語の原文を日本語に翻訳したものです。原文: https://adoptingerlang.org/docs/development/dependencies/

翻訳元: adoptingerlang/adoptingerlang 2025-12-18(コミット 899008f

過去20年で、プログラミングへの取り組み方には大きな変化がありました。 かつての資料の多くは、プロジェクト内で再利用可能で拡張しやすいコンポーネントを書くことに重点を置いていましたが、現在の潮流は、個別に使い捨てて置き換えやすい小さく独立したプロジェクトを作ることに移っています。 現在のマイクロサービス実装やJavaScriptの依存関係ツリーを見ていると、捨てるには小さすぎるプロジェクトなど存在しないとさえ思えてきます。

皮肉なことに、小さく単一目的のコンポーネントに必要だった強い分離やインターフェース、ネットワークAPI、プロトコルといった明確な境界への移行こそが、かつてないほどのコード再利用を生み出しました。 再利用が広まったのは、完璧に拡張可能なクラス階層を提供するという神話的な能力からではなく、さまざまな人やプロジェクトに共通するニーズからです。 今日では再利用があまりに進み、「再利用しすぎているのではないか」と問う人まで出てきました。 各ライブラリにはリスクと負債が伴い、開発を速く進めるためにその多くを引き受けているからです。

Erlangはもともと、関数型インターフェースで包んだ明確なメッセージパッシングプロトコルによる強い分離を核として育ってきた言語です。 経験豊富な開発者が中心の小さなコミュニティであることが、ライブラリやパッケージングの面で他の言語コミュニティに後れを取った一因でしょう。 ProcessOneのCEANやFaxienといったパッケージマネージャがOTPアプリケーションをグローバルにインストールする形で先行して試みられていましたが、他の開発者が作ったライブラリを気軽にインストールできるようになったのは2009年になってからのことです。 これを可能にしたのが最初のバージョンのrebarで、プロジェクトごとの依存関係インストールを重視し、開発者側でのインストールが不要な移植可能なスクリプトであるescriptとして配布された最初のツールでもありました。

ErlangのライブラリはGitHubをはじめとするホスティング型のバージョン管理サービス上に無秩序に増えていき、あるとき気づけば、同じ名前で似たようなバージョン番号を持ちながら細部の挙動が異なるPostgreSQLドライバが12種類以上見つかるという状態になっていました。

Erlangコミュニティが体制を立て直し、より分かりやすいエコシステムへとたどり着くには、mixやhex.pmといった新しい視点とツールをもたらしたElixirからのひと押しが必要でした。 今日のErlangコミュニティは依然として小規模ですが、より良いプラクティスを取り入れ、同じ仮想マシン上で動く数個の小規模言語やElixirとパッケージ・ライブラリ基盤を共有するまでになっています。

Erlangの世界で今どうライブラリを使い、コミュニティと足並みを揃えているかは、次の問いへの答えとして整理できます。

  • ライブラリとは何か
  • それを依存関係として使うにはどうすればよいか
  • 依存関係のライフサイクルとはどのようなものか
  • Elixirの依存関係はどう使えるか
  • モノレポで作業している場合はどうすればよいか

オープンソースライブラリを使う

Erlangのオープンソース依存関係は、リリースに含まれる他のライブラリと同じくただのOTPアプリケーションです。 そのため、オープンソースライブラリを使うのに必要なことは、そのOTPアプリケーションをErlangのツールチェーンから見える状態にするだけです。 考え方自体は単純ですが、この扱い方は言語やコミュニティによって微妙に異なります。 ライブラリをコンピュータ全体にグローバルインストールするか、共有環境内に置くか、プロジェクトごとにローカルインストールするかという選択肢があります。 さらに、バージョニングや公開に関する慣習も守る必要があります。 この節ではその具体的なやり方を示しますが、その前にRebar3がプロジェクトのライフサイクルについてどのような前提を置いているかを見ておきます。

Rebar3の前提

オープンソースでの作業にまつわる難しい判断のいくつかは、Rebar3の設計に刻み込まれています。 とくに使い始めたばかりのころは、逆らうより流れに乗ったほうが楽になることが多いでしょう。 Rebar3はもともと、オープンソースの依存関係を利用しながら自らも公開しつつ、一部のコードは永遠に非公開のままにしておくという半ばプライベートな形でサービスを書く中規模企業で作られたツールです。 複数の無関係なサービスが同時に開発され、それらすべてが同じErlangバージョンや同じライブラリで動くとは限りません。 プログラムの中にはローリングリスタートだけで済むもの(クラウドではごく普通のことです)もあれば、ホットコードロードがどうしても必要なものもあります。 Rebar3が生まれた当時は、コミュニティ内のバージョン管理の慣習もかなり乱雑でした。 多くのライブラリは.appファイル内のバージョンとGitHub上のgitタグのバージョンが食い違っており、ドキュメントにはさらに別のバージョンが書かれていることも珍しくありませんでした。 当時はhex.pm上のライブラリの5つに4つが、バージョン1.0.0にすら達していませんでした。

こうした経緯から、Rebar3には次のような性質があります。

  • Rebar3は宣言型のビルドツールです。設定を渡すと、それを満たすようにコードを実行します。カスタムスクリプトを実行してビルドを拡張したい場合は、専用のフックプラグインのインターフェースが用意されています。設定ファイルをより柔軟に組み立てるための動的な設定も利用できます。
  • この宣言型のアプローチの一部として、Rebar3のコマンドは依存関係の連なりとして定義されています。たとえばrebar3 compileタスクはrebar3 get-deps(正確には依存関係をロックする内部専用の形)に依存しており、これを自動的に実行してくれます。rebar3 tarコマンドはget-deps -> compile -> release -> tarという連なりを暗黙のうちに呼び出します。つまりRebar3は、プロジェクトをコンパイルするには依存関係が必要だということを理解しているのです。
  • Rebar3は_buildディレクトリ内に独自の作業領域を持ちます。ここに何が置かれるかはRebar3自身が管理するものであり、このディレクトリをソース管理下に置いたり内部構造に依存したりしないことが前提です。ユーザーが直接使いたくなる成果物を作った場合は、その成果物へのパスをターミナルに出力します。たとえばrebar3 escriptizeは実行のたびに_build/default/bin/に生成されたescriptへのパスを表示し、rebar3 ctはテストが失敗した場合にCommon TestのHTML出力へのパスを、rebar3 tarはリリースのtarballへのパスを表示します。
  • すべてのプロジェクトは、環境に現在読み込まれているErlangランタイム、つまり$PATHにあるerlをもとに、それぞれのディレクトリ内でローカルにビルドされます。
  • すべての依存関係はプロジェクトの_buildディレクトリに取得されます。
  • どの依存関係も自分自身の依存関係を定義でき、Rebar3はそれを認識してルートプロジェクトの_buildディレクトリにまとめて取得します。
  • バージョン番号は(セマンティックバージョニングを使っていても)信頼できないため、Rebar3はバージョンを人間向けの情報であってビルドツール向けの情報ではないと捉えています。同じライブラリが複数箇所で宣言された場合はプロジェクトルートに最も近い宣言が採用され、最初のビルド時(依存関係のロックがまだないため「最初」だと判断できます)には使われなかったバージョンをユーザーに知らせる警告が表示されます。これは、ルートプロジェクトに近い場所で宣言されたライブラリほど広く使われているはずだという前提に基づいています。
  • 循環依存は禁止されています。
  • Rebar3はプロジェクトごとに組み合わせ可能なプロファイルをサポートしており、テストでのみ使う依存関係や特定の環境(特定のターゲットOSなど)でのみ使う依存関係のように、設定を分割したり組み合わせたりできます。プロファイルの数や名前に制限はありませんが、defaulttestdocsprodの4つは特定のタスクのためにRebar3が自動的に使うプロファイルです。
  • ビルドは再現可能であることを前提としています。依存関係はロックされ、明示的に求めない限りアップグレードされません。rebar.lock内のバージョンは、rebar3 upgradeが呼ばれてロックファイルが更新されるまで、rebar.configで宣言されたバージョンより優先されます。
  • 依存関係は常にprodプロファイルを適用した状態でビルドされます。Rebar3は依存関係がロックファイルと一致していることやビルド成果物が存在することを毎回確認しますが、依存関係の中身を手で書き換えた変更までは検知しません。
  • Rebar3は、自分では管理できないライブラリの設定を調整したくなる場面があることを前提としており、そのためのオーバーライドをサポートしています。
  • このツールはgithg(mercurial)のようなバージョン管理を使って開発していることを前提としており、ブランチを切り替えるとロックファイル内の依存バージョンも切り替わることがあります。Rebar3はビルドのたびに依存関係を検証するため、ブランチ切り替えで変化があった場合は、現在のブランチにロックされたバージョンを取得するためにライブラリを自動的に再取得します。
  • コントリビューションや公開をしやすくするため、Rebar3はライブラリを相対パスで宣言する方法を標準ではサポートしていません。コードを公開する際にビルドが壊れやすく、再現不能で、可搬性のないものになりかねないからです。
  • プロジェクトの依存関係に変更を加える際に相対パスを使いたくなる場面が多いことは承知の上で、_checkoutsによって依存関係を一時的にローカルコピーへ自動的に置き換えられます。それ以外の用途では、プラグインによってカスタムのリソース型を作ることもできます。
  • Rebar3はエンドユーザー向けアプリケーションのインストーラでも実行環境でもなく、そうした用途は一切サポートしていません。目指しているのは、しかるべき専用のチャネルを通じてインストールできるビルド成果物を生成することです。Rebar3が本番のデバイスやサーバー上に存在する必要はありませんし、そうなることも想定していません。
  • Rebar3はサンドボックスではありません。ダウンロードする依存関係が正しい署名と一致していることを確認し、再現可能なビルドを提供しますが、ビルド中に呼び出されるスクリプトファイルやコンパイル中に実行されるパーストランスフォームが常に安全であることまでは保証できませんし、その責任を負うつもりもありません。プラグインも自動的にはロックされないため、コンパイルに影響するバージョンを固定するかどうかはライブラリの作者に委ねられています。

かなりの情報量ですが、依存関係に深く踏み込む前に知っておく価値はあります。 Rebar3がJavaScriptのnpmやElixirのmix、Goのツールチェーン、あるいは旧版のrebarと同じように動くという前提で操作すると、いくつかの挙動に戸惑うかもしれません。 そのことを踏まえたうえで、実際に依存関係を使ってみましょう。

依存関係の宣言

依存関係はすべてプロジェクトローカルなので、プロジェクトのrebar.configファイルで宣言する必要があります。 こうすることでRebar3は、それらを取得し、ビルドし、プロジェクトから使える状態にする必要があると認識します。 すべての依存関係は個別のOTPアプリケーションでなければならず、それによって互いに独立してバージョン管理・扱いができるようになっています。

次の形式が有効です。

{deps, [
    %% gitの依存関係
    {AppName, {git, "https://host.tld/path/to/app", {tag, "1.2.0"}}},
    {AppName, {git, "https://host.tld/path/to/app", {branch, "master"}}},
    {AppName, {git, "https://host.tld/path/to/app", {ref, "aed12f..."}}},
    %% mercurialの依存関係も同様の形式
    {AppName, {hg, "https://host.tld/path/to/app", {RefType, Ref}}}
    %% hexパッケージ
    AppName, % 判明している最新バージョン(`rebar3 update`時点)
    {AppName, "1.2.0"},
    {AppName, "~> 1.2.0"}, % 1.2.0以上1.3.0未満の最新バージョン
    {AppName, "1.2.0", {pkg, PkgName}}, % アプリケーションAppNameがパッケージ名PkgNameで公開されている場合
]}.

さらに、プラグインによってカスタムのリソース定義を定義し、プロジェクトに新しい種類の依存関係を追加することもできます。

本書のために作成したプロジェクト、service_discoveryを例に、実際の動きを見てみましょう。 rebar.configファイルを開くと、次のようになっています。

...

{deps, [
    {erldns,
     {git, "https://github.com/tsloughter/erldns.git",
     {branch, "revamp"}}},
    {dns,
     {git, "https://github.com/tsloughter/dns_erlang.git",
     {branch, "hex-deps"}}},

    recon,
    eql,
    jsx,
    {uuid, "1.7.5", {pkg, uuid_erl}},
    {elli, "~> 3.2.0"},
    {grpcbox, "~> 0.11.0"},
    {pgo,
     {git, "https://github.com/tsloughter/pgo.git",
     {branch, "master"}}}
]}.

...

gitの依存関係とhexの依存関係は、ほとんどのプロジェクトで併用できます。 唯一の例外はhexパッケージで、hexパッケージが依存できるのは他のhexパッケージだけです。 プロジェクト全体をコンパイルして、何が起きているかを順に見ていきましょう。

$ rebar3 compile
===> Fetching covertool v2.0.1
===> Downloaded package, caching at /Users/ferd/.cache/rebar3/hex/hexpm/packages/covertool-2.0.1.tar
===> Compiling covertool
...
===> Verifying dependencies...
===> Fetching dns (from {git,"https://github.com/tsloughter/dns_erlang.git",
               {ref,"abc562548e8a232289eec06cf96ce7066261cc9d"}})
===> Fetching provider_asn1 v0.2.3
===> Downloaded package, caching at /Users/ferd/.cache/rebar3/hex/hexpm/packages/provider_asn1-0.2.3.tar
===> Compiling provider_asn1
===> Fetching elli v3.2.0
...
===> Fetching rfc3339 v0.9.0
===> Version cached at /Users/ferd/.cache/rebar3/hex/hexpm/packages/rfc3339-0.9.0.tar is up to date, reusing it
===> Compiling quickrand
===> Compiling uuid
===> Compiling recon
...
===> Compiling service_discovery_storage
===> Compiling service_discovery
===> Compiling service_discovery_http
===> Compiling service_discovery_grpc
===> Compiling service_discovery_postgres

このビルドを実行すると、いくつかのことが起きているのが分かります。

  1. covertoolのようなプラグインが真っ先に取得・コンパイルされる
  2. プロジェクト本体の依存関係(dnselliなど)が取得される
  3. 依存関係(quickrandなど)がコンパイルされる
  4. メインのアプリケーションがコンパイルされる

rebar.lockファイルを削除してゼロからやり直すと、少し様子が変わります。 出力の一部として、次のようなものが見られるかもしれません。

...
===> Fetching dns (from {git,"https://github.com/tsloughter/dns_erlang.git",
               {branch,"hex-deps"}})
...
===> Skipping dns (from {git,"git://github.com/dnsimple/dns_erlang.git",
               {ref,"b9ee5b306acca34b3d866d183c475d5f12b313a5"}}) as an app of the same name has already been fetched
...
===> Skipping jsx v2.9.0 as an app of the same name has already been fetched
===> Skipping recon v2.4.0 as an app of the same name has already been fetched
...

これらは依存関係解決の過程で出る細かな通知や警告で、rebar.lockファイルがあれば不要になるものです。 競合が検出され、あるバージョンのライブラリがスキップされたことをライブラリの利用者に知らせています。 ビルドの監査を仕上げるには、rebar3 treeを呼び出して最終的な依存関係の解決結果を確認できます。

$ rebar3 tree
===> Verifying dependencies...
├─ service_discovery─e4b7061 (project app)
├─ service_discovery_grpc─e4b7061 (project app)
├─ service_discovery_http─e4b7061 (project app)
├─ service_discovery_postgres─e4b7061 (project app)
└─ service_discovery_storage─e4b7061 (project app)
   ├─ dns─0.1.0 (git repo)
   │  └─ base32─0.1.0 (hex package)
   ├─ elli─3.2.0 (hex package)
   ├─ eql─0.2.0 (hex package)
   ├─ erldns─1.0.0 (git repo)
   │  ├─ iso8601─1.3.1 (hex package)
   │  ├─ opencensus─0.9.2 (hex package)
   │  │  ├─ counters─0.2.1 (hex package)
   │  │  └─ wts─0.3.0 (hex package)
   │  │     └─ rfc3339─0.9.0 (hex package)
   │  └─ telemetry─0.4.0 (hex package)
   ├─ grpcbox─0.11.0 (hex package)
   │  ├─ acceptor_pool─1.0.0 (hex package)
   │  ├─ chatterbox─0.9.1 (hex package)
   │  │  └─ hpack─0.2.3 (hex package)
   │  ├─ ctx─0.5.0 (hex package)
   │  └─ gproc─0.8.0 (hex package)
   ├─ jsx─2.10.0 (hex package)
   ├─ pgo─0.8.0+build.91.refaf02392 (git repo)
   │  ├─ backoff─1.1.6 (hex package)
   │  └─ pg_types─0.0.0+build.24.ref32ed140 (git repo)
   ├─ recon─2.4.0 (hex package)
   └─ uuid─1.7.5 (hex package)
      └─ quickrand─1.7.5 (hex package)

この一覧はVerifying dependencies ...という行から始まりますが、これはツリーを表示する前にRebar3がすべての依存関係を解決済みであることを検証している部分です。 続いてトップレベルのアプリケーション(リポジトリ内で自分たちが書いているもの)が並び、その下に今取得した依存関係が続きます。 こうして解決ツリー全体を眺めることで、どのバージョンが取得され、どのアプリケーションがそれを持ち込んだのかが分かります。 2つ以上のアプリケーションが同じ推移的依存関係を含んでいる場合に、なぜそのバージョンが選ばれたのかを理解するのにも役立ちます。

_build/default/libの中を見てみると、これらのアプリケーションがそれぞれ自分のディレクトリを持っているのが分かります。

$ ls _build/default/lib
acceptor_pool  gproc       rfc3339
backoff        grpcbox     service_discovery
base32         hpack       service_discovery_grpc
chatterbox     iso8601     service_discovery_http
counters       jsx         service_discovery_postgres
ctx            opencensus  service_discovery_storage
dns            pgo         telemetry
elli           pg_types    uuid
eql            quickrand   wts
erldns         recon

これらはそれぞれ似たディレクトリ構造を持つOTPアプリケーションです。 このレイアウトは「OTPの全体像」で説明したリリース向けのプロジェクト構造とかなり似ていますが、ここはあくまでステージング領域にすぎません。

依存関係を含むプロジェクトのビルド

プロジェクト内のOTPアプリケーションをビルドするには、依存関係を取得してコンパイルするだけでは足りません。 「ライブラリをアプリにするもの」で触れたように、Erlangランタイムシステムは.appファイルの中に依存関係の実行時定義があることを前提としています。 そこに書かれていない依存関係は、実行時ではなくビルド時の依存関係だとRebar3に伝えることになります。 つまり、リリースには含まれず、rebar3 dialyzerの解析対象にも含まれないなど、一部のタスクや環境から除外されるのです。

apps/service_discovery/src/service_discovery.app.srcを開いて、applicationsタプルの値を見てみましょう。

{application, service_discovery,
 [{description, "Core functionality for service discovery service"},
  {vsn, {git, short}},
  {registered, []},
  {mod, {service_discovery_app, []}},
  {applications,
   [kernel,
    stdlib,
    erldns,
    service_discovery_storage
   ]},
  {env,[]},
  {modules, []},

  {licenses, ["Apache 2.0"]},
  {links, []}
 ]}.

erldnsservice_discovery_storageが追加されているのが分かります。 これらの依存関係を指定しておくことで、実行時やリリースの中でも確実に利用できるようになります。 書き忘れると、ビルドが壊れることがあります。

Erlangエコシステムの他のビルドツールを使ったことがあれば、こうした作業は経験したことがないかもしれません。 erlang.mkやElixirのMixといったツールは、プロジェクト設定内の依存関係をapplicationsタプルへ自動的にコピーしてくれます。 それを手作業でやるのは大きな手間に思えるかもしれませんが、これによってビルド時依存関係をサポートするOTPの標準に従うことになります。 他のツールも設定ファイル内の追加オプションによって似た結果にたどり着いています。 ここで、Rebar3のこのやり方が重要な制御をもたらす場面をいくつか見てみましょう。

1つ目の重要なケースは、デバッグに役立てるためにリリースへ含めたいものの、どのOTPアプリケーションからも依存されていない依存関係をダウンロードする場合です。 reconredbug、カスタムのloggerハンドラなどがその例です。 こうしたアプリケーションはリリースの中で使えるようにしておきたいものですが、applicationsタプルはアプリケーションを起動する順序をリリースに伝える役割も持つため、依存関係の連鎖には含めたくありません。 念のためインストールしたデバッグツールが、Webサイトを動かすために起動している必要があるでしょうか。 まったく必要ありません。 不具合のあるデバッグツールのせいで、本来のアプリケーションが起動できなくなるのは避けたいはずです。

このような場合には、service_discoveryの次の部分のようなプロジェクト設定にしておきたいところです。

{relx, [
    {release, {service_discovery, {git, long}},
     [service_discovery_postgres,
      service_discovery,
      service_discovery_http,
      service_discovery_grpc,
      recon]},
    ...
]}.

自分たちのアプリケーションと並んで、デバッグツールのreconがリリースのアプリケーション一覧に明示的に含まれているのが分かります。 (.appファイル内のapplicationsタプルに従って)推移的な依存関係もすべて含まれますが、個々のOTPアプリケーションは互いに独立して扱われます。

ここで、4つのservice_discovery系アプリケーションに少し注目してみましょう。 これらは、rebar.configでのビルド用の依存関係宣言と、.app.srcでの実行時の依存関係宣言を分けたくなる、2つ目のタイプの状況を表しています。

トップレベルでは、すべてのライブラリの依存関係が単一のrebar.configファイルにまとめて宣言されています。 これにより、開発者は必要なバージョンをまとめて管理・更新できます。 しかしservice_discoveryservice_discovery_httpそれぞれの.app.srcファイルを覗いてみると、次のようになっています。

%% service_discovery.app.src
...
  {applications,
   [kernel,
    stdlib,
    erldns,
    service_discovery_storage
   ]},
...
%% service_discovery_http.app.src
...
  {applications,
   [kernel,
    stdlib,
    service_discovery,
    jsx,
    elli
   ]},
...

service_discovery_httpはWebサーバー(elli)に依存していますが、service_discoveryは依存していません。 これにより、システムのバックエンドを起動するのにHTTPサーバーが立ち上がっている必要がないような、すっきりした起動・終了のシナリオが実現できます。

3つ目のシナリオとして、システムとやり取りしコマンドを送るためのescriptを生成するためだけに使う、service_discovery_mgmtという小さなアプリケーションを考えてみましょう。

もし実行時の依存関係が同じrebar.configファイルに依存するすべてのアプリケーションで共有されていたら、service_discovery_mgmtがリリースに含まれず単なる脇役のスクリプトだったとしても、他のアプリケーションが自動的に依存関係を持ち込んでしまい、本番環境にその依存関係が入り込むおそれがあります。 さらに悪いことに、service_discoveryリリースの依存関係がすべてそのスクリプトにも同梱されてしまうかもしれません。 ビルドツールが気を利かせようとした結果、Webサーバーやデータベースドライバまで抱え込んだ小さな管理ツールができあがりかねないのです。

そこでRebar3のメンテナは、プロジェクトのビルドや実行のために取得すべきアプリケーション(rebar.config側)と、各OTPアプリケーションの実行時の依存関係(.appファイル側、OTPの標準インストールに含まれるためrebar.configには書かれないこともあります)を、はっきり区別することに決めました。 エコシステム内の他のビルドツールも似た結果は実現できますが、たいていは実行時にすべてを含める方をデフォルトにしているのに対し、Rebar3は開発者に対して常に意図を明示するよう求めています。

ここまで整えておけば、あとは依存関係の集合を整理してきれいに保つだけです。

依存関係のライフサイクル

プロジェクトを初期化する際、もっとも重い作業になるのが依存関係の解決と取得です。 その結果はロックファイルに保存され、以後はRebar3を通じたより短い部分的な変更として扱われていきます。 この最初の段階は、頻繁に行うものではないとしても理解しておく価値があります。

Rebar3のロックファイルは、Rebar3を呼び出したディレクトリであるプロジェクトのルートに作成されます。 rebar.lockとして保存され、使っているバージョン管理システムで追跡しておくべきファイルです。 開いて中身を見ることはできますが、手で編集する必要は基本的にありません。 中身の大半はバージョン番号やアプリケーション名、各種のハッシュです。 時々監査してみるのも面白いかもしれませんが、依存関係ツリーを保守していく中で間接的に確認することになるでしょう。

ロックファイルは、ビルド時点で望んでいたすべての依存関係を平坦化したツリーを表しています。 変更を求めるか、ファイルを削除してゼロから解決をやり直させない限り、書き換わることはありません。 この厳格さは意図的なもので、Rebar3がどんな状況でも再現可能なビルドを保証できる理由の一部です。

ファイル内のハッシュのおかげで、依存関係が何層にも重なったミラー経由で取得されていても、また使用しているhexインデックスやgitソースのどこかで悪意ある第三者がパッケージを改ざんしていても、Rebar3は情報が想定と異なることを検知してエラーにできます。

したがって、ロックファイルは必要なときにだけ更新するべきです。 次の操作で更新できます。

  • rebar3 unlock <appname>は、使われなくなった依存関係をロックファイルから取り除きます。通常はrebar.configから先にその依存関係を削除したうえで呼び出し、それが本当になくなったか推移的依存関係に格下げされたことをRebar3に伝えるために使います。
  • rebar3 upgrade <appname>は、そのアプリケーションのロック済みバージョンを無視して、(あれば)rebar.configで現在指定されているバージョンから依存関係ツリーを再構築するようRebar3に指示します。新しいロックファイルが生成され、変わった可能性のある推移的依存関係もすべて再解決されます。
  • rebar3 updateはロックファイルそのものとは直接関係ありませんが、リモートのhexパッケージのローカルスナップショット(ビルドのたびにパッケージサーバーへ問い合わせずに済むキャッシュ)を更新します。あるアプリケーションでrebar3 upgradeを呼んでもHexにあるはずの最新バージョンに上がらない場合は、先にupdateを呼ぶ必要があります。Rebar3はローカルのインデックスキャッシュで依存関係を解決しようとすることでネットワークの利用を制限しているためです。rebar3 updateはインデックスに既にある各パッケージの最新エントリを取得し、その後もう一度upgradeを実行すれば最新バージョンが見えるようになります。なお、アップグレード先のバージョンを厳密に指定した場合は、ローカルだけでは依存関係を満たせないため、Rebar3が自動的に更新済みのインデックスを取得します。
  • rebar3 treeは、ビルドされてロックファイルに表されている依存関係ツリーを表示します。
  • rebar3 depsは依存関係を一覧表示し、更新できそうなものに注釈を付けます。ただし、何を更新対象とみなすかにはかなり強い制限があります。対象になるのはgitのブランチ、参照先が変わったタグ、バージョンを指定していないhexパッケージだけです。バージョンを"1.2.3"と指定していて1.2.4が使える場合には、何も知らせてくれません。

もう少し過激なコマンドとして、引数なしのrebar3 unlockrebar3 upgradeもあります。 これらは、次のビルドのためにロックファイルをまるごと取り除くだけのコマンドです。 とはいえ全体として、ここまでのコマンドがあればほとんどの依存関係の管理には十分でしょう。

一般的なワークフローは、次のようになるでしょう。

  1. 最初のプロジェクトをセットアップし、一度コンパイルして、ロックファイルをバージョン管理下に置く
  2. 変更したいトップレベルの依存関係が見つかる
  3. rebar.configファイル内の依存関係の定義を変更する(バージョン指定のないhexパッケージを使っている場合は、代わりにrebar3 updateを呼んでもかまいません)
  4. rebar3 upgrade <app>を呼んで、そのアプリケーションと推移的依存関係を更新する

これで完了です。

チェックアウト依存関係

Rebar3は、安全性と再現性を保ちながらも開発者の生活を楽にしたいと考えています。 _checkoutsは、ロックファイルのような再現性の考え方にあえて逆らう機能ですが、依存関係へのローカルな変更に対して素早いフィードバックとより良い体験をもたらします。

趣味であれ仕事であれ、Erlangを使い始めたばかりの段階では、依存関係が別リポジトリに分かれているプロジェクトはそう多くないでしょう。 それらを扱うのはさほど難しくありません。 しかし遅かれ早かれ、依存関係にパッチを当て始めたり、何十ものリポジトリを抱える企業環境で作業したりするようになると、Rebar3を使う作業がもどかしく感じられることがあります。

問題は、依存関係に手を加えて試したいだけなのに、そのたびに変更をコミットしてRebar3が取得できる場所に公開しなければならないことです。 _build配下のソースファイルに対応する.beamファイルが既にある場合、Rebar3はそのファイルへの変更をビルドしないためです。 リポジトリの境界をまたいで作業し、ただ変更を試したいだけのときに、これはすぐに煩わしくなります。

そこで用意されているのが、チェックアウト依存関係というちょっとした裏技的な機能です。 チェックアウト依存関係は次のように働きます。

  • ファイルシステム上のどこかにメインプロジェクトがある
  • その依存関係がメインプロジェクトのrebar.configで宣言されている
  • その依存関係自体も、単独のプロジェクトとしてファイルシステム上のどこかにある
  • メインプロジェクトに_checkouts/ディレクトリを追加する
  • 依存関係のディレクトリを_checkouts/ディレクトリにコピーするかシンボリックリンクする

この時点から、メインアプリをビルドするたびに、checkouts内の依存関係のディレクトリにebin/ディレクトリが追加され、メインプロジェクト内のトップレベルアプリケーションであるかのように再コンパイルされます。

こうすることで、準備が整うまでメインプロジェクトの中で依存関係への変更を試せます。 作業が終わったら、依存関係のebin/ディレクトリを削除し、依存関係側のコードをコミット・公開して_checkoutsディレクトリから取り除き、rebar3 upgradeします。

これにより、依存関係の変更をプッシュしたり、依存関係をローカルディレクトリに向けるよう設定ファイルを書き換えたりすることなく、メインプロジェクトの文脈の中で依存関係に対する小さな反復的変更を数多く試せます。 アプリケーションごとにリポジトリを分ける開発戦略全体のオーバーヘッドを、大きく減らしてくれるのです。

Elixirの依存関係を使う

長年、ElixirとErlangの関係は一方通行でした。 ElixirプロジェクトにErlangの依存関係を含めることはできても、その逆はできなかったのです。 その後、Erlang Ecosystem Foundationの支援もあって、Rebar3にまったく新しいコンパイラ管理の仕組みが加えられました。 この仕組みのおかげで、Erlangの利用者がElixirのコードを使えるようにするコンパイラプラグインを書けるようになりました。

まずElixirをインストールします。 Elixir公式サイトの手順に従うとよいでしょう。 多くのElixir開発者はバージョン管理にasdfを使っています。 「セットアップ」で説明したように、Erlang用のkerlオプションはasdfのErlangプラグインと組み合わせて使えるので、これで環境を一通り揃えられます。

Elixirを用意できたら、ライブラリやプロジェクトにrebar_mixプラグインを追加します。

{plugins, [rebar_mix]}.
{provider_hooks, [{post, [{compile, {mix, consolidate_protocols}}]}]}.

{relx, [
    % ...
    {overlay, [
        {copy, "{{base_dir}}/consolidated", "releases/{{release_version}}/consolidated"}
    ]}
}.

vm.args.srcファイルがあれば、次の行も追加します。

-pa releases/${REL_VSN}/consolidated

この時点から、Elixirのコードを含むhexの依存関係を問題なくインストールできるようになります。 現時点でこのプラグインがサポートするのは、他のhex依存関係にのみ頼っているhex依存関係だけですが、gitを使った推移的依存関係のサポートも近く追加される見込みです。

なお、同じライブラリの中でErlangとElixirの両方を使う混在プロジェクトは、Rebar3側・Elixir側の双方でさらに作業が必要なため、現時点ではサポートされていません。 一方、Mixはこのパターンが必要であればサポートしています。

企業環境での利用

企業環境には、何ができて何ができないかについて奇妙な制約がいろいろとあり、開発ツールもかなり独特になりがちです。 Rebar3は主にオープンソースの世界に合うように開発されており、プロジェクト構造の強制と依存関係の取得、そしてその両方を組み合わせて標準的なツール群をラップする口実にすることに主眼を置いています。

そのため、このツールを企業環境に適合させるのが少々骨の折れる作業になったとしても不思議ではありません。 ここでは、企業環境でよく使われ、Erlangを採用する際に役立つであろう標準的なツールをいくつか取り上げます。

プロキシのサポート

多くの職場では非常に厳格なファイアウォールのルールが敷かれており、送受信されるデータをすべて検査・監視することさえあります。 たいていの場合、こうした環境が公衆インターネットから完全に切り離されているわけではありませんが、外向きの接続にはプロキシサーバーの利用が求められます。

HTTP_PROXYHTTPS_PROXYという環境変数をプログラムが尊重するのは、ごく標準的なことです。 開発環境でこれらが設定されていれば、Rebar3は外部と通信する際に必ずこれらのプロキシを経由するようにします。

これで、IT部門のポリシーに沿って問題なく作業できるはずです。 ただし、それだけでは足りない場合もあります。

プライベートHexミラー

企業によっては、社内ネットワークを公衆インターネットからさらに一歩踏み込んで分離しているところもあります。 社内に持ち込まれるデータはすべて検査・独立ホスティングされ、GitHubやGitLab、hexのような共通のコードリポジトリと通信する余地はありません。 もう一つの興味深いケースがビルドサーバーで、安全性と再現性の両方の理由から外部との接続を一切遮断したい場合があります。

このような構成では、次節で扱うモノレポでのベンダリングと、ここで扱うプライベートホストのパッケージインデックスという、2つのやり方がよく使われます。

プライベートホストのインデックスの発想は、プロジェクトで使うすべてのパッケージと依存関係を完全に審査するというものです。 コード品質のための技術レビューやセキュリティ評価を行ったり、ライセンスや特許の問題について企業の法務担当者にコードを確認してもらったりすることもあるでしょう。 そのうえで、正しいバージョンのパッケージだけが使えるようになります。 この種のインデックスは、各ビルドサーバー上でローカルに動くか、ビルドサーバーと同じくらい厳重に監視されたプライベートネットワーク内で動くという前提のもと、隔離されたビルドのために広く受け入れられています。

Rebar3はこの用途をサポートしています。 有効にするには、まずプライベートなhexインスタンスを立てる必要があります。 これはminirepoプロジェクトで実現できます。 プロジェクトページの手順に従えば、ローカルファイルシステムかS3ベースのストレージを使い、他のインデックスをミラーしつつ自分のパッケージをプライベートに公開できる、自前のプライベートhexサーバーが手に入ります。

これを利用するにはグローバルなRebar3の設定を調整する必要がありますが、それさえ済めば準備は完了です。

モノレポについて

プライベートなライブラリやOTPアプリケーション、依存関係のすべてを対等に扱うモノレポ(単なるアンブレラリリースとは違います)の企業環境で作業しているなら、Rebar3は最適なツールとは言えません。 これは主に、モノレポを使う企業の多くが非常に独自のワークフローを持つカスタムツール群と大規模なコードベースを抱えており、Rebar3のメンテナにアクセス権を共有することはまずないという事情によるものです。 メンテナがアクセスできるようにならない限り、この面でできることはほとんどありません。

モノレポを使いながらもRebar3に頼っている商用ユーザーの中には、_checkouts依存関係を使いつつ_buildディレクトリを再設定する組み合わせでビルドに成功したという報告もあります。 とはいえ、現時点ではこのやり方を推奨することはできませんし、モノレポへの公式なサポートも提供されていません。

もう一つの選択肢は依存関係をベンダリングすることで、rebar3_path_depsのようなプラグインを通じて行えます。

ここまで整えれば、プロジェクトの依存関係のライフサイクルを管理する準備は整っているはずです。