Code Loading

Note

この章では、パッケージの読み込みに関する技術的な詳細を説明します。パッケージをインストールするには、Pkgを使用して、アクティブな環境にパッケージを追加します。アクティブな環境に既にあるパッケージを使用するには、import Xまたはusing Xと記述します。これは、Modules documentationで説明されています。

Definitions

ジュリアにはコードを読み込むための2つのメカニズムがあります:

  1. コードのインクルージョン: 例: include("source.jl")。インクルージョンを使用すると、単一のプログラムを複数のソースファイルに分割できます。include("source.jl")という式は、include呼び出しが行われるモジュールのグローバルスコープでsource.jlファイルの内容を評価させます。include("source.jl")が複数回呼び出されると、source.jlは複数回評価されます。インクルードされたパスsource.jlは、include呼び出しが行われるファイルに対して相対的に解釈されます。これにより、ソースファイルのサブツリーを簡単に移動できます。REPLでは、インクルードされたパスは現在の作業ディレクトリに対して相対的に解釈されます。pwd()
  2. パッケージの読み込み: 例: import X または using X。インポートメカニズムは、パッケージ—すなわち、モジュールにラップされた独立した再利用可能なJuliaコードのコレクション—を読み込むことを可能にし、インポートするモジュール内でXという名前で結果のモジュールを利用可能にします。同じXパッケージが同じJuliaセッションで複数回インポートされる場合、最初のインポート時にのみ読み込まれます—その後のインポートでは、インポートするモジュールは同じモジュールへの参照を取得します。ただし、import Xは異なるコンテキストで異なるパッケージを読み込むことができることに注意してください: Xはメインプロジェクト内のXという名前の1つのパッケージを指すことができますが、各依存関係内で同じ名前の異なるパッケージを指す可能性もあります。詳細は以下に記載します。

コードのインクルージョンは非常に簡単で明確です:指定されたソースファイルを呼び出し元のコンテキストで評価します。パッケージのロードはコードのインクルージョンの上に構築されており、different purposeに役立ちます。この章の残りの部分は、パッケージのロードの動作とメカニズムに焦点を当てています。

パッケージは、他のJuliaプロジェクトで再利用可能な機能を提供する標準的なレイアウトを持つソースツリーです。パッケージはimport Xまたはusing Xステートメントによってロードされます。これらのステートメントは、パッケージコードをロードすることによって得られるXという名前のモジュールを、インポートステートメントが発生するモジュール内で利用可能にします。import XにおけるXの意味は文脈に依存します:どのXパッケージがロードされるかは、そのステートメントが発生するコードによって決まります。したがって、import Xの処理は二段階で行われます。まず、この文脈でXとして定義されているのパッケージかを特定し、次に、その特定のXパッケージがどこにあるかを特定します。

これらの質問は、LOAD_PATH にリストされているプロジェクト環境を検索することで回答されます。プロジェクトファイル(Project.toml または JuliaProject.toml)、マニフェストファイル(Manifest.toml または JuliaManifest.toml、または特定のバージョンのために -v{major}.{minor}.toml でサフィックスされた同じ名前)、またはソースファイルのフォルダを探します。

Federation of packages

ほとんどの場合、パッケージはその名前から一意に識別できます。しかし、時にはプロジェクトが同じ名前を持つ2つの異なるパッケージを使用する必要がある状況に直面することがあります。1つのパッケージの名前を変更することでこれを修正できるかもしれませんが、そうすることを強いられると、大規模で共有されたコードベースでは非常に混乱を招く可能性があります。代わりに、Juliaのコード読み込みメカニズムは、同じパッケージ名がアプリケーションの異なるコンポーネントで異なるパッケージを指すことを可能にします。

Juliaは、連邦パッケージ管理をサポートしており、これは複数の独立した当事者が公共およびプライベートのパッケージとパッケージのレジストリを維持できることを意味し、プロジェクトは異なるレジストリからの公共およびプライベートのパッケージの混合に依存できることを意味します。さまざまなレジストリからのパッケージは、共通のツールとワークフローを使用してインストールおよび管理されます。Juliaに付属するPkgパッケージマネージャーを使用すると、プロジェクトの依存関係をインストールおよび管理できます。これは、プロジェクトが依存する他のプロジェクトを説明するプロジェクトファイルの作成と操作を支援し、プロジェクトの完全な依存関係グラフの正確なバージョンをスナップショットするマニフェストファイルを作成します。

連邦の一つの結果は、パッケージ名のための中央権限が存在しないことです。異なる組織が無関係なパッケージを指すために同じ名前を使用することがあります。この可能性は避けられません。なぜなら、これらの組織は調整を行わず、お互いの存在すら知らないかもしれないからです。中央の命名権限がないため、単一のプロジェクトが同じ名前を持つ異なるパッケージに依存することがあるかもしれません。Juliaのパッケージ読み込みメカニズムは、パッケージ名が単一のプロジェクトの依存関係グラフ内でさえもグローバルに一意である必要はありません。代わりに、パッケージは universally unique identifiers (UUID) によって識別され、各パッケージが作成されるときに割り当てられます。通常、これらのやや扱いにくい128ビットの識別子を直接扱う必要はありません。なぜなら、Pkg がそれらの生成と追跡を行ってくれるからです。しかし、これらのUUIDは X はどのパッケージを指しているのか?」 という質問に対する決定的な答えを提供します。

分散型命名問題はやや抽象的であるため、具体的なシナリオを通じて問題を理解するのが役立つかもしれません。あなたが App というアプリケーションを開発しているとしましょう。このアプリケーションは、PubPriv の2つのパッケージを使用しています。Priv はあなたが作成したプライベートパッケージであり、Pub はあなたが使用しているが制御していないパブリックパッケージです。あなたが Priv を作成したとき、Priv という名前のパブリックパッケージは存在していませんでした。しかし、その後、無関係なパッケージが Priv という名前で公開され、人気を博しました。実際、Pub パッケージはそれを使用し始めました。したがって、次に Pub をアップグレードして最新のバグ修正や機能を取得すると、App はあなたの行動とは無関係に、2つの異なる Priv パッケージに依存することになります。App はあなたのプライベートな Priv パッケージに直接依存しており、Pub を通じて新しいパブリックな Priv パッケージに間接的に依存しています。これらの2つの Priv パッケージは異なりますが、App が正しく動作し続けるためには両方とも必要です。そのため、import Priv という表現は、App のコード内で発生するか、Pub のコード内で発生するかによって異なる Priv パッケージを参照しなければなりません。これを処理するために、Julia のパッケージ読み込みメカニズムは、UUID によって2つの Priv パッケージを区別し、その文脈(import を呼び出したモジュール)に基づいて正しいものを選択します。この区別がどのように機能するかは、以下のセクションで説明される環境によって決まります。

Environments

環境は、さまざまなコードコンテキストにおける import Xusing X の意味、およびこれらのステートメントがどのファイルをロードするかを決定します。Juliaは2種類の環境を理解しています:

  1. プロジェクト環境は、プロジェクトファイルとオプションのマニフェストファイルを含むディレクトリであり、明示的な環境を形成します。プロジェクトファイルは、プロジェクトの直接依存関係の名前とアイデンティティを決定します。マニフェストファイルが存在する場合、すべての直接および間接依存関係、各依存関係の正確なバージョン、正しいバージョンを特定して読み込むための十分な情報を含む完全な依存関係グラフを提供します。
  2. パッケージディレクトリは、一連のパッケージのソースツリーをサブディレクトリとして含むディレクトリであり、暗黙の環境を形成します。もし X がパッケージディレクトリのサブディレクトリであり、X/src/X.jl が存在する場合、パッケージディレクトリ環境内でパッケージ X が利用可能であり、X/src/X.jl がそれがロードされるソースファイルです。

これらはスタック環境を作成するために混合することができます:プロジェクト環境とパッケージディレクトリの順序付けられたセットが重ね合わされて、単一の合成環境を作ります。その後、優先順位と可視性のルールが組み合わさって、どのパッケージが利用可能で、どこからロードされるかを決定します。たとえば、Juliaのロードパスはスタック環境を形成します。

これらの環境はそれぞれ異なる目的に役立ちます:

  • プロジェクト環境は再現性を提供します。プロジェクト環境をバージョン管理にチェックインすることで—例えば、gitリポジトリに—プロジェクトのソースコードの残りとともに、プロジェクトの正確な状態とそのすべての依存関係を再現できます。特にマニフェストファイルは、ソースツリーの暗号学的ハッシュによって特定されたすべての依存関係の正確なバージョンをキャプチャし、Pkgが正しいバージョンを取得し、すべての依存関係に対して記録された正確なコードを実行していることを確認できるようにします。
  • パッケージディレクトリは、便利さを提供します。これは、完全に注意深く追跡されたプロジェクト環境が不要な場合に役立ちます。パッケージのセットをどこかに置いて、プロジェクト環境を作成することなく直接使用できるようにしたいときに便利です。
  • スタック環境は、ツールを追加することを可能にします。開発ツールの環境をスタックの末尾にプッシュすることで、REPLやスクリプトから利用できるようになりますが、パッケージ内からは利用できません。

高レベルでは、各環境は概念的に3つのマップを定義します:ルーツ、グラフ、パスです。import Xの意味を解決する際に、ルーツとグラフのマップがXのアイデンティティを決定するために使用され、パスのマップがXのソースコードを特定するために使用されます。3つのマップの具体的な役割は次のとおりです:

  • ルーツ: name::Symboluuid::UUID

    環境のルーツマップは、環境がメインプロジェクトに提供するすべてのトップレベルの依存関係に対して、パッケージ名をUUIDに割り当てます(つまり、Mainでロードできるもの)。Juliaがメインプロジェクトでimport Xに遭遇すると、Xの識別子をroots[:X]として検索します。

  • グラフ: context::UUIDname::Symboluuid::UUID

    環境のグラフは、各 context UUID に対して名前から UUID へのマップを割り当てる多層マップであり、ルーツマップに似ていますが、その context に特有のものです。Julia が UUID が context のパッケージのコード内で import X を見たとき、X のアイデンティティを graph[context][:X] として検索します。特に、これは import Xcontext に応じて異なるパッケージを参照できることを意味します。

  • パス: uuid::UUID × name::Symbolpath::String

    パスマップは、各パッケージのUUID-名前ペアに、そのパッケージのエントリーポイントソースファイルの場所を割り当てます。import XにおけるXの識別が、ルーツまたはグラフを介してUUIDに解決されると(それがメインプロジェクトから読み込まれるか、依存関係から読み込まれるかに応じて)、Juliaは環境内のpaths[uuid,:X]を参照してXを取得するために読み込むべきファイルを決定します。このファイルを含めることで、Xという名前のモジュールが定義されるはずです。このパッケージが読み込まれると、その後の同じuuidに解決されるインポートは、すでに読み込まれたパッケージモジュールへの新しいバインディングを作成します。

各種環境は、以下のセクションで詳述されているように、これらの3つのマップを異なって定義します。

Note

この章の例では、ルート、グラフ、パスの完全なデータ構造を示しており、理解を容易にしています。しかし、Juliaのパッケージロードコードはこれらを明示的に作成するわけではありません。代わりに、特定のパッケージをロードするために必要な分だけ、各構造を遅延的に計算します。

Project environments

プロジェクト環境は、Project.tomlというプロジェクトファイルを含むディレクトリによって決定され、オプションでManifest.tomlというマニフェストファイルも含まれます。これらのファイルは、JuliaProject.tomlおよびJuliaManifest.tomlと呼ばれることもあり、その場合はProject.tomlおよびManifest.tomlは無視されます。これにより、Project.tomlおよびManifest.tomlと呼ばれるファイルを重要視する他のツールとの共存が可能になります。ただし、純粋なJuliaプロジェクトの場合、Project.tomlおよびManifest.tomlという名前が好まれます。しかし、Julia v1.10.8以降、(Julia)Manifest-v{major}.{minor}.tomlは、特定のマニフェストファイルを使用するための形式として認識されます。つまり、同じフォルダー内で、Manifest-v1.11.tomlはv1.11によって使用され、Manifest.tomlは他の任意のJuliaバージョンによって使用されます。

プロジェクト環境のルーツ、グラフ、パスマップは次のように定義されています:

環境のルーツマップは、プロジェクトファイルの内容によって決定されます。具体的には、最上位のnameおよびuuidエントリと[deps]セクション(すべてオプション)です。以下は、前述の仮想アプリケーションAppの例のプロジェクトファイルです:

name = "App"
uuid = "8f986787-14fe-4607-ba5d-fbff2944afa9"

[deps]
Priv = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
Pub  = "c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"

このプロジェクトファイルは、次のルートマップを示唆しています。もしそれがJuliaの辞書で表現されていたら:

roots = Dict(
    :App  => UUID("8f986787-14fe-4607-ba5d-fbff2944afa9"),
    :Priv => UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"),
    :Pub  => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
)

与えられたルーツマップにおいて、Appのコード内のimport Privという文は、Juliaがroots[:Priv]を参照することを引き起こし、これによりba13f791-ae1d-465a-978b-69c3ad90f72bが得られます。これは、そのコンテキストで読み込まれるPrivパッケージのUUIDです。このUUIDは、メインアプリケーションがimport Privを評価する際に、どのPrivパッケージを読み込んで使用するかを特定します。

プロジェクト環境の依存関係グラフは、マニフェストファイルの内容によって決まります。マニフェストファイルが存在しない場合、グラフは空になります。マニフェストファイルには、プロジェクトの直接または間接の依存関係ごとにスタンザが含まれています。各依存関係について、ファイルにはパッケージのUUIDとソースコードへのソースツリーのハッシュまたは明示的なパスがリストされます。以下は、Appのための例のマニフェストファイルです:

[[Priv]] # the private one
deps = ["Pub", "Zebra"]
uuid = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
path = "deps/Priv"

[[Priv]] # the public one
uuid = "2d15fe94-a1f7-436c-a4d8-07a9a496e01c"
git-tree-sha1 = "1bf63d3be994fe83456a03b874b409cfd59a6373"
version = "0.1.5"

[[Pub]]
uuid = "c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"
git-tree-sha1 = "9ebd50e2b0dd1e110e842df3b433cb5869b0dd38"
version = "2.1.4"

  [Pub.deps]
  Priv = "2d15fe94-a1f7-436c-a4d8-07a9a496e01c"
  Zebra = "f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"

[[Zebra]]
uuid = "f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"
git-tree-sha1 = "e808e36a5d7173974b90a15a353b564f3494092f"
version = "3.4.2"

このマニフェストファイルは、Appプロジェクトの可能な完全依存関係グラフを説明しています:

  • アプリケーションが使用する Priv という名前の異なる2つのパッケージがあります。1つはルート依存関係であるプライベートパッケージで、もう1つは Pub を通じての間接依存関係であるパブリックパッケージです。これらはそれぞれ異なるUUIDによって区別されており、異なる依存関係を持っています:

    • プライベート PrivPubZebra パッケージに依存しています。
    • パブリック Priv は依存関係がありません。
  • アプリケーションはまた、Pub パッケージに依存しており、これはさらに公開されている Priv と、プライベートな Priv パッケージが依存しているのと同じ Zebra パッケージに依存しています。

この依存関係グラフは辞書として表現され、次のようになります:

graph = Dict(
    # Priv – the private one:
    UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b") => Dict(
        :Pub   => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
        :Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
    ),
    # Priv – the public one:
    UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c") => Dict(),
    # Pub:
    UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1") => Dict(
        :Priv  => UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"),
        :Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
    ),
    # Zebra:
    UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62") => Dict(),
)

与えられたこの依存関係 graph において、Julia が Pub パッケージ(UUID c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1)内で import Priv を見たとき、次のように検索します:

graph[UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1")][:Priv]

そして 2d15fe94-a1f7-436c-a4d8-07a9a496e01c を取得し、これは Pub パッケージの文脈において import Priv がアプリが直接依存しているプライベートなものではなく、パブリックな Priv パッケージを指すことを示しています。これにより、名前 Priv がメインプロジェクトの異なるパッケージと、そのパッケージの依存関係の一つで異なるパッケージを指すことができ、パッケージエコシステム内での重複した名前を可能にします。

import ZebraがメインのAppコードベースで評価されるとどうなりますか?Zebraがプロジェクトファイルに表示されていないため、インポートは失敗しますが、Zebraはマニフェストファイルには表示されています。さらに、import ZebraがUUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01cの公開Privパッケージで発生した場合、それも失敗します。なぜなら、そのPrivパッケージにはマニフェストファイルに宣言された依存関係がないため、パッケージをロードできないからです。Zebraパッケージは、マニフェストファイルに明示的な依存関係として表示されているパッケージ、つまりPubパッケージおよびいずれかのPrivパッケージによってのみロードできます。

プロジェクト環境のパスマップはマニフェストファイルから抽出されます。パッケージuuidの名前Xのパスは、以下のルール(順番に)によって決定されます:

  1. ディレクトリ内のプロジェクトファイルが uuid と名前 X に一致する場合、次のいずれか:

    • それにはトップレベルの path エントリがあり、その後 uuid はプロジェクトファイルを含むディレクトリに対して相対的に解釈されるそのパスにマッピングされます。
    • そうでなければ、uuid はプロジェクトファイルを含むディレクトリに対して相対的に src/X.jl にマッピングされます。
  2. もし上記が当てはまらず、プロジェクトファイルに対応するマニフェストファイルがあり、マニフェストに uuid と一致するスタンザが含まれている場合は:

    • path エントリがある場合は、そのパスを使用します(マニフェストファイルを含むディレクトリに対して相対的です)。
    • git-tree-sha1 エントリがある場合、uuidgit-tree-sha1 の決定論的ハッシュ関数を計算し、それを slug と呼びます。そして、Julia の DEPOT_PATH グローバル配列内の各ディレクトリで packages/X/$slug という名前のディレクトリを探します。存在する最初のディレクトリを使用します。

成功した場合、ソースコードエントリポイントへのパスは、その結果、またはその結果からの相対パスに src/X.jl を追加したものになります。そうでなければ、uuid に対するパスマッピングは存在しません。X をロードする際に、ソースコードパスが見つからない場合、ルックアップは失敗し、ユーザーは適切なパッケージバージョンをインストールするように促されるか、他の修正アクション(例:X を依存関係として宣言する)を取るように求められることがあります。

上記の例のマニフェストファイルでは、最初の Priv パッケージ—UUID ba13f791-ae1d-465a-978b-69c3ad90f72b のもの—のパスを見つけるために、Julia はマニフェストファイル内のそのスタンザを探し、path エントリがあることを確認し、App プロジェクトディレクトリに対して相対的に deps/Priv を見ます—App コードが /home/me/projects/App にあると仮定しましょう—/home/me/projects/App/deps/Priv が存在することを確認し、したがってそこから Priv をロードします。

もし、逆にジュリアが別の Priv パッケージ(UUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01cのもの)を読み込んでいる場合、マニフェストの中でそのスタンザを見つけ、path エントリがないことを確認し、git-tree-sha1 エントリがあることを確認します。次に、このUUID/SHA-1ペアのslugを計算します。これはHDkrTです(この計算の正確な詳細は重要ではありませんが、一貫して決定論的です)。これは、このPrivパッケージへのパスがpackages/Priv/HDkrT/src/Priv.jlになることを意味します。DEPOT_PATHの内容が["/home/me/.julia", "/usr/local/julia"]であると仮定すると、ジュリアは次のパスを見て、それらが存在するかどうかを確認します:

  1. /home/me/.julia/packages/Priv/HDkrT
  2. /usr/local/julia/packages/Priv/HDkrT

ジュリアは、見つかったデポ内のファイル packages/Priv/HDKrT/src/Priv.jl から公開の Priv パッケージを読み込むために、これらのうち最初に存在するものを使用します。

ここに、上記のマニフェストで提供された依存関係グラフのために、ローカルファイルシステムを検索した後の例の App プロジェクト環境の可能なパスマップの表現があります:

paths = Dict(
    # Priv – the private one:
    (UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"), :Priv) =>
        # relative entry-point inside `App` repo:
        "/home/me/projects/App/deps/Priv/src/Priv.jl",
    # Priv – the public one:
    (UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"), :Priv) =>
        # package installed in the system depot:
        "/usr/local/julia/packages/Priv/HDkr/src/Priv.jl",
    # Pub:
    (UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"), :Pub) =>
        # package installed in the user depot:
        "/home/me/.julia/packages/Pub/oKpw/src/Pub.jl",
    # Zebra:
    (UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"), :Zebra) =>
        # package installed in the system depot:
        "/usr/local/julia/packages/Zebra/me9k/src/Zebra.jl",
)

この例のマップには、3種類のパッケージの場所が含まれています(最初と3番目はデフォルトのロードパスの一部です):

  1. プライベート Priv パッケージは "vendored" で、App リポジトリ内にあります。
  2. システムデポには、公開された PrivZebra パッケージがあり、ここにはシステム管理者によってインストールおよび管理されているパッケージが存在します。これらはシステム上のすべてのユーザーが利用可能です。
  3. Pub パッケージはユーザーデポにあり、ユーザーがインストールしたパッケージが存在します。これらはインストールしたユーザーのみが利用可能です。

Package directories

パッケージディレクトリは、名前の衝突を処理する能力なしに、よりシンプルな環境を提供します。パッケージディレクトリでは、トップレベルのパッケージのセットは、"パッケージのように見える" サブディレクトリのセットです。パッケージディレクトリにパッケージ X が存在するのは、ディレクトリに以下のいずれかの "エントリポイント" ファイルが含まれている場合です:

  • X.jl
  • X/src/X.jl
  • X.jl/src/X.jl

パッケージディレクトリ内のパッケージがインポートできる依存関係は、そのパッケージにプロジェクトファイルが含まれているかどうかによって異なります:

  • プロジェクトファイルがある場合、そのファイルの [deps] セクションで特定されたパッケージのみをインポートできます。
  • プロジェクトファイルがない場合、任意のトップレベルパッケージをインポートできます。つまり、MainやREPLで読み込むことができるのと同じパッケージです。

ルーツマップは、パッケージディレクトリの内容を調べて、存在するすべてのパッケージのリストを生成することによって決定されます。さらに、各エントリには次のようにUUIDが割り当てられます: フォルダーX内で見つかった特定のパッケージについて...

  1. X/Project.toml が存在し、uuid エントリがある場合、uuid はその値です。
  2. X/Project.toml が存在し、かつ最上位の UUID エントリがない場合、uuidX/Project.toml の標準的(実際の)パスをハッシュ化して生成されたダミー UUID です。
  3. そうでない場合(Project.toml が存在しない場合)、uuid はすべてゼロの nil UUID です。

プロジェクトディレクトリの依存関係グラフは、各パッケージのサブディレクトリにあるプロジェクトファイルの存在と内容によって決まります。ルールは次のとおりです:

  • パッケージのサブディレクトリにプロジェクトファイルがない場合、そのディレクトリはグラフから省略され、そのコード内のインポート文はメインプロジェクトやREPLと同様にトップレベルとして扱われます。
  • パッケージのサブディレクトリにプロジェクトファイルがある場合、そのUUIDのグラフエントリはプロジェクトファイルの[deps]マップであり、セクションが存在しない場合は空であると見なされます。

例として、パッケージディレクトリが以下の構造と内容を持っているとします:

Aardvark/
    src/Aardvark.jl:
        import Bobcat
        import Cobra

Bobcat/
    Project.toml:
        [deps]
        Cobra = "4725e24d-f727-424b-bca0-c4307a3456fa"
        Dingo = "7a7925be-828c-4418-bbeb-bac8dfc843bc"

    src/Bobcat.jl:
        import Cobra
        import Dingo

Cobra/
    Project.toml:
        uuid = "4725e24d-f727-424b-bca0-c4307a3456fa"
        [deps]
        Dingo = "7a7925be-828c-4418-bbeb-bac8dfc843bc"

    src/Cobra.jl:
        import Dingo

Dingo/
    Project.toml:
        uuid = "7a7925be-828c-4418-bbeb-bac8dfc843bc"

    src/Dingo.jl:
        # no imports

ここに辞書として表現された対応するルーツ構造があります:

roots = Dict(
    :Aardvark => UUID("00000000-0000-0000-0000-000000000000"), # no project file, nil UUID
    :Bobcat   => UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"), # dummy UUID based on path
    :Cobra    => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"), # UUID from project file
    :Dingo    => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"), # UUID from project file
)

ここに辞書として表現された対応するグラフ構造があります:

graph = Dict(
    # Bobcat:
    UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf") => Dict(
        :Cobra => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"),
        :Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"),
    ),
    # Cobra:
    UUID("4725e24d-f727-424b-bca0-c4307a3456fa") => Dict(
        :Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"),
    ),
    # Dingo:
    UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc") => Dict(),
)

いくつかの一般的なルールに注意してください:

  1. プロジェクトファイルのないパッケージは、任意のトップレベルの依存関係に依存することができ、パッケージディレクトリ内のすべてのパッケージがトップレベルで利用可能であるため、環境内のすべてのパッケージをインポートすることができます。
  2. プロジェクトファイルを持つパッケージは、プロジェクトファイルを持たないパッケージに依存することはできません。なぜなら、プロジェクトファイルを持つパッケージは graph 内のパッケージのみをロードでき、プロジェクトファイルを持たないパッケージは graph に表示されないからです。
  3. プロジェクトファイルはあるが明示的なUUIDがないパッケージは、プロジェクトファイルのないパッケージのみが依存できる。なぜなら、これらのパッケージに割り当てられたダミーUUIDは厳密に内部的なものであるからだ。

以下の具体的な例を観察してください。

  • アードバークボブキャットコブラ、またはディンゴのいずれかをインポートできます; 実際にボブキャットコブラをインポートします。
  • BobcatCobraDingo の両方をインポートでき、実際にインポートします。これらはどちらも UUID を持つプロジェクトファイルを持ち、Bobcat[deps] セクションで依存関係として宣言されています。
  • BobcatAardvark に依存できません。なぜなら Aardvark にはプロジェクトファイルがないからです。
  • CobraDingo をインポートすることができ、実際にインポートします。Dingo にはプロジェクトファイルとUUIDがあり、Cobra[deps] セクションで依存関係として宣言されています。
  • CobraAardvarkBobcatに依存できません。なぜなら、どちらも実際のUUIDを持っていないからです。
  • Dingo[deps] セクションのないプロジェクトファイルを持っているため、何もインポートできません。

パスマップはパッケージディレクトリ内でシンプルです:サブディレクトリ名をそれに対応するエントリポイントパスにマッピングします。言い換えれば、例のプロジェクトディレクトリへのパスが/home/me/animalsである場合、pathsマップはこの辞書で表現できます:

paths = Dict(
    (UUID("00000000-0000-0000-0000-000000000000"), :Aardvark) =>
        "/home/me/AnimalPackages/Aardvark/src/Aardvark.jl",
    (UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"), :Bobcat) =>
        "/home/me/AnimalPackages/Bobcat/src/Bobcat.jl",
    (UUID("4725e24d-f727-424b-bca0-c4307a3456fa"), :Cobra) =>
        "/home/me/AnimalPackages/Cobra/src/Cobra.jl",
    (UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"), :Dingo) =>
        "/home/me/AnimalPackages/Dingo/src/Dingo.jl",
)

パッケージディレクトリ環境内のすべてのパッケージは、定義上、期待されるエントリポイントファイルを持つサブディレクトリであるため、paths マップエントリは常にこの形式になります。

Environment stacks

第三の最終的な環境の種類は、いくつかの環境を重ね合わせて他の環境を組み合わせるものであり、各環境のパッケージを単一の複合環境で利用可能にします。これらの複合環境は環境スタックと呼ばれます。JuliaのLOAD_PATHグローバルは、環境スタックを定義します—Juliaプロセスが動作する環境です。Juliaプロセスが特定のプロジェクトまたはパッケージディレクトリ内のパッケージにのみアクセスできるようにしたい場合は、それをLOAD_PATHの唯一のエントリにしてください。しかし、作業しているプロジェクトの依存関係でなくても、お気に入りのツール—標準ライブラリ、プロファイラ、デバッガ、個人ユーティリティなど—にアクセスできることは非常に便利です。これらのツールを含む環境をロードパスに追加することで、プロジェクトに追加することなく、トップレベルのコードでそれらにすぐにアクセスできます。

環境スタックのコンポーネントのルート、グラフ、パスデータ構造を組み合わせるメカニズムはシンプルです:それらは辞書としてマージされ、キーの衝突が発生した場合には後のエントリよりも前のエントリが優先されます。言い換えれば、stack = [env₁, env₂, …] がある場合、次のようになります:

roots = reduce(merge, reverse([roots₁, roots₂, …]))
graph = reduce(merge, reverse([graph₁, graph₂, …]))
paths = reduce(merge, reverse([paths₁, paths₂, …]))

rootsᵢgraphᵢ、および pathsᵢ という添字付き変数は、stack に含まれる添字付き環境 envᵢ に対応しています。reverse が存在するのは、merge が引数の辞書内のキーに衝突がある場合、最初の引数よりも最後の引数を優先するためです。この設計にはいくつかの注目すべき特徴があります:

  1. プライマリ環境—すなわちスタック内の最初の環境—は、スタックされた環境に忠実に埋め込まれています。スタック内の最初の環境の完全な依存関係グラフは、すべての依存関係の同じバージョンを含む形で、スタックされた環境に完全に含まれることが保証されています。
  2. 非プライマリ環境のパッケージは、自身の環境が完全に互換性があっても、依存関係の互換性のないバージョンを使用することがあります。これは、依存関係の1つがスタック内の以前の環境のバージョンによって影に隠されるときに発生する可能性があります(グラフまたはパス、またはその両方によって)。

プロジェクトの主要な環境は通常、あなたが取り組んでいるプロジェクトの環境であり、スタックの後半にある環境には追加のツールが含まれているため、これは正しいトレードオフです:開発ツールを壊すよりもプロジェクトを動作させ続ける方が良いです。このような非互換性が発生した場合、通常はメインプロジェクトと互換性のあるバージョンに開発ツールをアップグレードしたいと思うでしょう。

Package Extensions

パッケージ「extension」は、指定された一連の他のパッケージ(その「トリガー」)が現在のJuliaセッションで読み込まれたときに自動的に読み込まれるモジュールです。拡張機能はプロジェクトファイルの[extensions]セクションで定義されます。拡張機能のトリガーは、プロジェクトファイルの[weakdeps](および場合によっては、あまり一般的ではありませんが[deps])セクションにリストされているパッケージのサブセットです。これらのパッケージは、他のパッケージと同様に互換性エントリを持つことができます。

name = "MyPackage"

[compat]
ExtDep = "1.0"
OtherExtDep = "1.0"

[weakdeps]
ExtDep = "c9a23..." # uuid
OtherExtDep = "862e..." # uuid

[extensions]
BarExt = ["ExtDep", "OtherExtDep"]
FooExt = "ExtDep"
...

extensionsの下のキーは拡張機能の名前です。それらは、その拡張機能の右側にあるすべてのパッケージ(トリガー)が読み込まれたときに読み込まれます。拡張機能にトリガーが1つだけの場合、トリガーのリストは簡潔に文字列として書くことができます。拡張機能のエントリポイントの場所は、ext/FooExt.jlまたはext/FooExt/FooExt.jlのいずれかです(拡張機能FooExtの場合)。拡張機能の内容はしばしば次のように構成されています:

module FooExt

# Load main package and triggers
using MyPackage, ExtDep

# Extend functionality in main package with types from the triggers
MyPackage.func(x::ExtDep.SomeStruct) = ...

end

パッケージに拡張機能が追加されると、そのパッケージのセクションに weakdepsextensions セクションがマニフェストファイルに保存されます。パッケージの依存関係の検索ルールは、その「親」と同じですが、リストされたトリガーも依存関係として考慮されます。

Package/Environment Preferences

Preferencesは、環境内でのパッケージの動作に影響を与えるメタデータの辞書です。Preferencesシステムは、コンパイル時にPreferencesを読み取ることをサポートしており、これはコード読み込み時に、Juliaによって選択されたプリコンパイルファイルが現在の環境と同じPreferencesで構築されていることを確認する必要があることを意味します。Preferencesを変更するための公開APIは、Preferences.jlパッケージ内に含まれています。Preferencesは、現在アクティブなプロジェクトの隣にある(Julia)LocalPreferences.tomlファイル内にTOML辞書として保存されます。もしPreferenceが「エクスポート」される場合、それは代わりに(Julia)Project.toml内に保存されます。この意図は、共有プロジェクトが共有Preferencesを含むことを可能にし、ユーザー自身がLocalPreferences.tomlファイル内で自分の設定でそれらのPreferencesを上書きできるようにすることです。このファイルは、その名前が示すように.gitignoreされるべきです。

コンパイル中にアクセスされる設定は自動的にコンパイル時設定としてマークされ、これらの設定に記録された変更は、Juliaコンパイラがそのモジュールのキャッシュされたプリコンパイルファイル(.jiおよび対応する.so.dll、または.dylibファイル)を再コンパイルする原因となります。これは、コンパイル中にすべてのコンパイル時設定のハッシュをシリアライズし、そのハッシュを適切なファイルをロードする際に現在の環境と照合することによって行われます。

デポ全体のデフォルトで設定された設定を使用できます。パッケージ Foo がグローバル環境にインストールされており、設定がある場合、これらの設定はグローバル環境が LOAD_PATH の一部である限り適用されます。環境スタックの上位にある環境の設定は、ロードパス内のより近いエントリによって上書きされ、現在アクティブなプロジェクトで終了します。これにより、デポ全体の設定デフォルトが存在し、アクティブなプロジェクトがこれらの継承された設定をマージしたり、完全に上書きしたりできるようになります。設定をマージを許可または不許可にする方法の詳細については、Preferences.set_preferences!() のドキュメント文字列を参照してください。

Conclusion

連邦パッケージ管理と正確なソフトウェア再現性は、パッケージシステムにおいて困難ではあるが価値のある目標です。これらの目標を組み合わせることで、ほとんどの動的言語が持つよりも複雑なパッケージロードメカニズムが生まれますが、それはまた、静的言語に一般的に関連付けられるスケーラビリティと再現性をもたらします。通常、Juliaユーザーは、これらの相互作用を正確に理解することなく、組み込みのパッケージマネージャを使用してプロジェクトを管理できるはずです。Pkg.add("X")の呼び出しは、Pkg.activate("Y")を介して選択された適切なプロジェクトおよびマニフェストファイルに追加されるため、将来のimport Xの呼び出しは、さらなる考慮なしにXをロードします。