Документация Engee

Загрузка кода

В этой главе рассматриваются технические особенности загрузки пакетов. Для установки пакетов используйте Pkg — встроенный диспетчер пакетов Julia, который позволяет добавлять пакеты в активную среду. Для использования пакетов, уже имеющихся в активной среде, служит оператор import X или using X; см. документацию по модулям.

Определения

В Julia есть два механизма загрузки кода.

  1. Включение кода, например include("source.jl"). Включение позволяет разделять программу на несколько файлов исходного кода. При использовании выражения include("source.jl") содержимое файла source.jl вычисляется в глобальной области модуля, в котором происходит вызов include. Если include("source.jl") вызывается несколько раз, столько же раз вычисляется файл source.jl. Путь включения source.jl интерпретируется относительно файла, в котором происходит вызов include. Это позволяет легко перемещать поддерево файлов исходного кода. В REPL пути включения интерпретируются относительно текущего рабочего каталога pwd().

  2. Загрузка пакета, например import X или using X. Механизм импорта позволяет загрузить пакет — независимый, повторно используемый блок кода на Julia, заключенный в модуль, — и сделать этот модуль доступным по имени X внутри импортирующего модуля. Если в течение сеанса Julia один и тот же пакет X импортируется несколько раз, он загружается только в первый раз — в дальнейшем импортирующий модуль получает ссылку на него. Однако имейте в виду, что оператор import X в разных контекстах может загружать разные пакеты: в главном проекте X может ссылаться на один пакет с именем X, а в зависимостях — на другие пакеты тоже с именем X. Дополнительные сведения см. ниже.

Включение кода происходит очень просто: указанный файл исходного кода вычисляется в контексте вызывающего модуля. Загрузка пакетов основана на включении кода, но служит другой цели. В остальной части этой главы рассматриваются принципы действия загрузки пакетов.

Пакет — это дерево исходного кода со стандартной структурой, предоставляющее функции, которые можно использовать в других проектах Julia. Пакет загружается с помощью оператора import X или using X. Они делают пакет с именем X доступным в модуле, где был вызван оператор импорта. Значение X в выражении import X зависит от контекста: загружаемый пакет X зависит от кода, в котором используется оператор. Таким образом, обработка выражения import X происходит в два этапа: сначала определяется то, какой именно пакет является пакетом X в данном контексте, а затем то, где находится этот пакет X.

Для ответа на эти вопросы производится поиск в средах проекта, перечисленных в константе LOAD_PATH для файлов проекта (Project.toml или JuliaProject.toml), файлов манифеста (Manifest.toml или JuliaManifest.toml) или папок с файлами исходного кода.

Федерация пакетов

Как правило, пакет уникально идентифицируется по имени. Однако иногда в проекте может потребоваться использовать два разных пакета с одинаковыми именами. Хотя для этого можно просто переименовать один из пакетов, такое действие может вызвать серьезные проблемы в большой общей базе кода. Поэтому механизм загрузки кода в Julia позволяет использовать одно и то же имя для ссылки на разные пакеты в разных частях приложения.

Julia поддерживает федеративное управление пакетами. Это означает, что разные независимые стороны могут вести собственные реестры общедоступных и частных пакетов, а в проектах может использоваться комбинация общедоступных и частных пакетов из разных реестров. Для установки пакетов из разных реестров и управления ими применяется общий набор инструментов и рабочих процессов. Диспетчер пакетов Pkg, который поставляется с Julia, позволяет устанавливать зависимости проектов и управлять ими. Он дает возможность создавать файлы проектов (описывающие другие проекты, от которых зависит ваш проект) и файлы манифеста (в которых фиксируются версии полного графа зависимостей вашего проекта), а также работать с ними.

Одним из следствий федерации является отсутствие централизованной системы именования пакетов. Разные объекты могут использовать одно и то же имя для ссылки на не связанные друг с другом пакеты. Это неизбежно, так как объекты не согласуют свои действия и могут даже не знать о существовании друг друга. Из-за отсутствия централизованной системы именования в одном проекте в итоге могут использоваться разные пакеты с одинаковыми именами. Механизм загрузки пакетов в Julia не требует, чтобы имена пакетов были глобально уникальными даже в пределах графа зависимостей одного проекта. Вместо этого пакеты определяются по универсальным уникальным идентификаторам (UUID), которые присваиваются каждому пакету при его создании. Обычно работать напрямую с этими достаточно громоздкими 128-битными идентификаторами напрямую не требуется, так как задачу их генерирования и отслеживания берет на себя Pkg. Однако благодаря UUID можно получить однозначный ответ на вопрос «на какой пакет ссылается X

Так как проблема децентрализованного именования достаточно абстрактная, для ее понимания может быть полезно разобрать конкретную ситуацию. Предположим, вы разрабатываете приложение под названием App, в котором используются два пакета: Pub и Priv. Priv — это частный пакет, который вы создали сами, а Pub — общедоступный пакет, который вы используете, но не контролируете. Когда вы создавали пакет Priv, общедоступного пакета с именем Priv не было. Однако в дальнейшем кем-то был создан совершенно другой пакет Priv, который был опубликован и стал популярен. Более того, он начал использоваться в пакете Pub. Поэтому, когда вы в следующий раз обновите Pub, чтобы получить последние функции и исправить ошибки, в App в итоге будут использоваться два разных пакета с именем Priv, и это лишь в результате обновления. Приложение App имеет прямую зависимость от вашего частного пакета Priv и косвенную зависимость, посредством Pub, от нового общедоступного пакета Priv. Так как эти пакеты Priv разные, но оба они требуются для правильной работы App, выражение import Priv должно ссылаться на разные пакеты Priv в зависимости от того, где оно применяется: в коде App или в коде Pub. Для этого механизм загрузки пакетов в Julia различает два пакета Priv по идентификаторам UUID и выбирает нужный в зависимости от контекста (модуля, вызвавшего оператор import). То, как это происходит, зависит от среды и описывается в следующих разделах.

Среды

Среда определяет, что значат операторы import X и using X в различных контекстах кода и какие файлы они загружают. В Julia есть два типа сред.

  1. Среда проекта — это каталог с файлом проекта и, возможно, файлом манифеста. Они образуют явную среду. Файл проекта определяет имена и идентификаторы прямых зависимостей проекта. В файле манифеста при его наличии описывается полный граф зависимостей, включая все прямые и косвенные зависимости, точные версии каждой зависимости и сведения, необходимые для нахождения и загрузки правильной версии.

  2. Каталог пакетов содержит вложенные каталоги с деревьями исходного кода набора пакетов. Он образует неявную среду. Если X — это вложенный каталог в каталоге пакетов и существует файл X/src/X.jl, значит пакет X доступен в среде каталога пакетов, а X/src/X.jl — это файл исходного кода, посредством которого он загружается.

Эти среды можно использовать в сочетании для создания многослойной среды: упорядоченного набора сред проекта и каталогов пакетов, которые накладываются друг на друга для формирования единой составной среды. В этом случае правила приоритета и видимости, определяющие то, какие пакеты доступны и откуда они загружаются, комбинируются. Например, путь загрузки Julia образует многослойную среду.

Каждая из этих сред служит своей цели.

  • Среды проектов обеспечивают воспроизводимость. Добавив среду проекта в систему управления версиями, например в репозиторий Git, вместе с остальным исходным кодом проекта, можно воспроизвести точное состояние проекта и всех его зависимостей. В частности, в файле манифеста фиксируется точная версия каждой зависимости, которая идентифицируется по криптографическому хэшу дерева исходного кода. Это позволяет диспетчеру Pkg получать точные версии и загружать для каждой зависимости именно тот код, который нужно.

  • Каталоги пакетов обеспечивают удобство, когда требуется тщательно отслеживаемая среда проекта. Они полезны в том случае, если нужно разместить где-либо набор пакетов и использовать их напрямую, не создавая для них среду проекта.

  • Многослойные среды позволяют добавлять инструменты в основную среду. Вы можете поместить среду инструментов разработки в конец стека, чтобы они были доступны из REPL и скриптов, но не из пакетов.

На самом общем уровне каждая среда определяет три схемы: корней, графа и путей. При определении значения выражения import X с помощью схем корней и графа идентифицируется пакет X, а с помощью схемы путей устанавливается местонахождение его исходного кода. Ниже подробно описывается назначение каждой из трех схем.

  • roots (корни): name::Symboluuid::UUID

    Схема корней среды присваивает имена пакетов идентификаторам UUID всех зависимостей верхнего уровня, которые среда делает доступными главному проекту (то есть которые можно загружать в Main). Когда Julia встречает выражение import X в главном проекте, она идентифицирует X как roots[:X].

  • graph (граф): context::UUIDname::Symboluuid::UUID

    Граф среды — это многоуровневая схема, которая назначает каждому UUID в контексте (context) сопоставление имен с UUID. Она работает так же, как схема корней, но в пределах данного контекста (context). Когда Julia встречает выражение import X в коде пакета с UUID context, она идентифицирует X как graph[context][:X]. В частности, это означает, что выражение import X может ссылаться на разные пакеты в зависимости от context.

  • paths (пути): uuid::UUID × name::Symbolpath::String

    Схема путей назначает каждой паре UUID и имени пакета расположение, где находится файл исходного кода с точкой входа в этот пакет. После того как элемент X в выражении import X был разрешен в идентификатор UUID посредством схемы корней или графа (в зависимости от того, происходит ли загрузка из главного проекта или из зависимости), Julia определяет, какой файл нужно загрузить, чтобы получить X, путем поиска по paths[uuid,:X] в среде. При включении этого файла определяется модуль с именем X. После загрузки пакета все последующие операции импорта, разрешающиеся в тот же идентификатор uuid, будут приводить к созданию привязки к уже загруженному модулю пакета.

Для среды каждого типа эти три схемы определяются по-разному, как описывается в следующих разделах.

Для большей наглядности в примерах в этой главе представлены полные структуры данных для схем корней, графа и путей. Однако код загрузки пакетов Julia не создает их явным образом. Вместо этого он «лениво» вычисляет только ту часть каждой структуры, которая достаточна для загрузки данного пакета.

Среды проектов

Среда проекта образуется из каталога, содержащего файл проекта с именем Project.toml, и необязательного файла манифеста с именем Manifest.toml. Эти файлы также могут иметь имена JuliaProject.toml и JuliaManifest.toml; в этом случае файлы Project.toml и Manifest.toml игнорируются. Это позволяет одновременно использовать инструменты, полагающиеся на файлы с именами Project.toml и Manifest.toml. Однако для проектов исключительно на Julia имена Project.toml и Manifest.toml предпочтительнее.

Схемы корней, графа и путей среды проекта определяются следующим образом.

Схема корней среды определяется содержимым файла проекта, в частности его элементами 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"),
)

При такой схеме корней оператор import Priv в коде App предписывает Julia выполнить поиск roots[:Priv], в результате чего будет получено значение ba13f791-ae1d-465a-978b-69c3ad90f72b — UUID пакета Priv, который должен быть загружен в данном контексте. Идентификатор UUID определяет то, какой пакет Priv должен загружаться и использоваться при вычислении оператора import Priv в главном приложении.

Граф зависимостей среды проекта определяется содержимым файла манифеста при его наличии. Если файла манифеста нет, граф пуст. В файле манифеста имеется отдельный раздел для каждой прямой или косвенной зависимости проекта. Для каждой зависимости в файле указываются UUID пакета и хэш дерева исходного кода или явный путь к исходному коду. Рассмотрим следующий пример файла манифеста для приложения App:

[[Priv]] # частный
deps = ["Pub", "Zebra"]
uuid = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
path = "deps/Priv"

[[Priv]] # общедоступный
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: частный, являющийся корневой зависимостью, и общедоступный, являющийся косвенной зависимостью через Pub. Они различаются по уникальным идентификаторам UUID и имеют разные зависимости.

    • Частный пакет Priv зависит от пакетов Pub и Zebra.

    • У общедоступного пакета Priv зависимостей нет.

  • Приложение также зависит от пакета Pub, который, в свою очередь, зависит от общедоступного пакета Priv и от того же пакета Zebra, от которого зависит частный пакет Priv.

Граф зависимостей, представленный в виде словаря, выглядит так:

graph = Dict(
    # Частный пакет Priv:
    UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b") => Dict(
        :Pub   => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
        :Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
    ),
    # Общедоступный пакет Priv:
    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 встречает выражение import Priv в пакете Pub с UUID c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1, производится следующий поиск:

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 использовался в общедоступном пакете Priv с UUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01c, он также завершился бы ошибкой, так как у этого пакета Priv нет объявленных зависимостей в файле манифеста и поэтому он не может загружать другие пакеты. Пакет Zebra может загружаться только пакетами, для которых он явно указан как зависимость в файле манифеста, а именно пакетом Pub и одним из пакетов Priv.

Схема путей среды проекта извлекается из файла манифеста. Путь к пакету с идентификатором uuid и именем X определяется по следующим правилам (в указанном порядке).

  1. Если файл проекта в каталоге соответствует идентификатору uuid и имени X, то происходит следующее. — Если у него есть элемент path верхнего уровня, то uuid сопоставляется с этим путем, который интерпретируется относительно каталога, содержащего файл проекта. — В противном случае uuid сопоставляется с путем src/X.jl относительно каталога, содержащего файл проекта.

  2. Если перечисленные выше условия не соблюдаются, но для файла проекта есть файл манифеста, в котором есть раздел, соответствующий идентификатору uuid, происходит следующее. — Если в нем есть элемент path, используется этот путь (относительно каталога, содержащего файл манифеста). — Если в нем есть элемент git-tree-sha1, для uuid и значения git-tree-sha1 вычисляется детерминированная хэш-функция и с учетом ее результата (назовем его slug) в каждом каталоге в глобальном массиве DEPOT_PATH Julia ищется каталог packages/X/$slug. Будет использоваться первый такой найденный каталог.

Если какое-либо из этих условий выполнено, путем к точке входа в исходный код будет либо полученный результат, либо путь относительно этого результата с добавлением src/X.jl. В противном случае пути, соответствующего идентификатору uuid, нет. Если при загрузке пакета X не удается найти путь к исходному коду, поиск завершается сбоем и пользователю может быть предложено установить соответствующую версию пакета или исправить ситуацию иным образом (например, объявить X как зависимость).

В примере файла манифеста выше путь к первому пакету Priv (с UUID ba13f791-ae1d-465a-978b-69c3ad90f72b) определяется следующим образом: Julia ищет раздел этого пакета в файле манифеста, находит элемент path, ищет путь deps/Priv относительно каталога проекта App (допустим, что код App находится по пути /home/me/projects/App), находит каталог /home/me/projects/App/deps/Priv и поэтому загружает пакет Priv из него.

Если же требовалось бы загрузить другой пакет Priv (с UUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01c), среда Julia нашла бы его раздел в манифесте, в котором, однако, нет элемента path, но есть элемент git-tree-sha1. Поэтому было бы вычислено значение slug для этой пары UUID и хэша SHA-1 и получен результат HDkrT (как именно производится это вычисление, неважно, но функция согласованная и детерминированная). Это означает, что путем к пакету Priv будет путь packages/Priv/HDkrT/src/Priv.jl в одном из хранилищ пакетов. Допустим, переменная DEPOT_PATH содержит значение ["/home/me/.julia", "/usr/local/julia"]. В этом случае Julia проверяет, существуют ли следующие пути:

  1. /home/me/.julia/packages/Priv/HDkrT

  2. /usr/local/julia/packages/Priv/HDkrT

Затем Julia пытается загрузить общедоступный пакет Priv из файла packages/Priv/HDKrT/src/Priv.jl в хранилище, в котором найден первый из этих путей.

Ниже показана возможная схема путей для примера среды проекта App в том виде, в каком она представлена в манифесте для приведенного выше графа зависимостей, после поиска в локальной файловой системе.

paths = Dict(
    # Частный пакет Priv:
    (UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"), :Priv) =>
        # относительная точка входа внутри репозитория `App`:
        "/home/me/projects/App/deps/Priv/src/Priv.jl",
    # Общедоступный пакет Priv:
    (UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"), :Priv) =>
        # пакет, установленный в системном хранилище:
        "/usr/local/julia/packages/Priv/HDkr/src/Priv.jl",
    # Pub:
    (UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"), :Pub) =>
        # пакет, установленный в пользовательском хранилище:
        "/home/me/.julia/packages/Pub/oKpw/src/Pub.jl",
    # Zebra:
    (UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"), :Zebra) =>
        # пакет, установленный в системном хранилище:
        "/usr/local/julia/packages/Zebra/me9k/src/Zebra.jl",
)

В схеме из примера есть расположения пакетов трех разных типов (первое и третье расположения являются частью пути загрузки по умолчанию).

  1. Собственная копия частного пакета Priv «размещается» в репозитории App.

  2. Общедоступные пакеты Priv и Zebra находятся в системном хранилище, в котором устанавливает пакеты и управляет ими системный администратор. Они доступны всем пользователям системы.

  3. Пакет Pub находится в пользовательском хранилище, где пакеты устанавливаются активным пользователем. Они доступны только установившему их пользователю.

Каталоги пакетов

Каталоги пакетов — это более простой тип среды, не позволяющий разрешать конфликты имен. В каталоге пакетов набор пакетов верхнего уровня — это набор вложенных каталогов, которые «выглядят» как пакеты. Пакет X существует в каталоге пакетов, если в этом каталоге есть один из следующих файлов точки входа:

  • X.jl

  • X/src/X.jl

  • X.jl/src/X.jl

То, какие зависимости может импортировать пакет в каталоге пакетов, определяется наличием файла проекта в пакете.

  • Если файл проекта есть, импортировать можно только пакеты, указанные в его разделе [deps].

  • Если файла проекта нет, импортировать можно любые пакеты верхнего уровня, то есть те же пакеты, которые можно загружать в модуле Main или в REPL.

Схема корней определяется путем анализа содержимого каталога проектов и формирования списка всех имеющихся пакетов. Кроме того, каждому пакету, найденному в папке X, назначается идентификатор UUID следующим образом.

  1. Если имеется файл X/Project.toml и в нем есть элемент uuid, то этот элемент uuid и будет значением идентификатора.

  2. Если файл X/Project.toml имеется, но в нем нет элемента UUID верхнего уровня, фиктивный идентификатор uuid генерируется путем хэширования канонического (реального) пути к X/Project.toml.

  3. В противном случае (если файла Project.toml нет), идентификатор 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:
        # импортов нет

В виде словаря соответствующая структура корней будет представлена так:

roots = Dict(
    :Aardvark => UUID("00000000-0000-0000-0000-000000000000"), # файла проекта нет, нулевой UUID
    :Bobcat   => UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"), # фиктивный UUID на основе пути
    :Cobra    => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"), # UUID из файла проекта
    :Dingo    => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"), # UUID из файла проекта
)

В виде словаря соответствующая структура графа будет представлена так:

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, а пакетов без файлов проекта в ней нет.

  3. От пакета с файлом проекта, но без явно заданного UUID могут зависеть только пакеты без файлов проекта, так как присваиваемые таким пакетам фиктивные UUID предназначены исключительно для внутреннего использования.

Посмотрим, как эти правила применяются на практике в нашем примере.

  • Aardvark может импортировать любой из пакетов Bobcat, Cobra или Dingo; фактически импортируются Bobcat и Cobra.

  • Bobcat может импортировать и импортирует пакеты Cobra и Dingo, у которых нет файлов проекта с UUID и которые объявлены как зависимости в разделе [deps] пакета Bobcat.

  • Bobcat может зависеть от Aardvark, так как у пакета Aardvark нет файла проекта.

  • Cobra может импортировать и импортирует пакет Dingo, у которого есть файл проекта и UUID и который объявлен как зависимость в разделе [deps] пакета Cobra.

  • Cobra не может зависеть от пакета Aardvark или Bobcat, так как у них нет реальных идентификаторов 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 всегда имеют такую форму.

Стеки сред

Третий и последний тип среды — это такой, в котором среды других типов накладываются друг на друга, образуя единую среду, где доступны пакеты каждой из составляющих сред. Такие составные среды называются стеками сред. В глобальной переменной 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ᵢ с нижними индексами соответствуют средам envᵢ с нижними индексами, входящими в stack. Функция reverse применяется по той причине, что при совпадении ключей в словарях, переданных в качестве аргументов в функцию merge, приоритет отдается последнему аргументу, а не первому. У такого подхода есть несколько заслуживающих внимания особенностей.

  1. Основная среда, то есть первая среда в стеке, включается в многослойную среду полностью. Граф зависимостей первой среды в стеке гарантированно включается в многослойную среду полностью и без изменений, в том числе с теми же версиями всех зависимостей.

  2. Пакеты из неосновных сред в итоге могут использовать несовместимые версии зависимостей, даже если их собственные среды полностью совместимы. Это может произойти, если одна из их зависимостей заменяется версией из более ранней среды в стеке (в соответствии с графом, путем или и тем, и другим).

Так как основная среда — это обычно среда текущего проекта, а более поздние среды в стеке содержат дополнительные инструменты, такой компромисс вполне оправдан: лучше, чтобы перестали работать инструменты разработки, чем весь проект. Как правило, при возникновении таких несовместимостей необходимо обновить инструменты разработки до версий, совместимых с главным проектом.

Расширения пакетов

Расширение пакета — это модуль, который автоматически загружается при загрузке определенного набора других пакетов («зависимостей расширения») в текущем сеансе Julia. Зависимости расширения — это подмножество других пакетов, перечисленных в разделе [weakdeps] файла проекта. Расширения определяются в файле проекта в разделе [extensions]:

name = "MyPackage"

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

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

Ключи в разделе extensions — это имена расширений. Расширение загружается при загрузке всех пакетов, указанных в правой части (зависимостей расширения). Если у расширения всего одна зависимость, список зависимостей для краткости можно записать в виде строки. Точка входа для расширения FooExt находится в ext/FooExt.jl или ext/FooExt/FooExt.jl. Содержимое расширения часто имеет такую структуру:

module FooExt

# Загрузка главного пакета и зависимостей расширения
using MyPackage, ExtDep

# Расширение функциональности главного пакета посредством типов из зависимостей расширения
MyPackage.func(x::ExtDep.SomeStruct) = ...

end

Когда пакет с расширениями добавляется в среду, разделы weakdeps и extensions сохраняются в файле манифеста в разделе для этого пакета. Правила поиска зависимостей для пакета такие же, как и для его «родителя», за тем исключением, что зависимости расширения в списке также считаются зависимостями.

Предпочтения в отношении пакетов и сред

Предпочтения — это словари метаданных, влияющих на поведение пакетов в среде. Система предпочтений поддерживает чтение предпочтений во время компиляции. Это означает, что перед загрузкой кода необходимо убедиться в том, что сборка файлов предварительной компиляции, выбранных Julia, была выполнена с предпочтениями текущей среды. Общедоступный API для изменения предпочтений содержится в пакете Preferences.jl. Предпочтения хранятся в виде словарей TOML в файле (Julia)LocalPreferences.toml рядом с текущим активным проектом. При экспорте предпочтение переносится в файл (Julia)Project.toml. Замысел в том, чтобы общие проекты могли иметь общие предпочтения, но пользователи могли переопределять их с помощью собственных настроек в файле LocalPreferences.toml, который, как следует из его имени, должен добавляться в файл .gitignore.

Предпочтения, к которым происходит обращение во время компиляции, автоматически помечаются как предпочтения времени компиляции. Любое их изменение вынудит компилятор Julia перекомпилировать все кэшированные файлы предварительной компиляции (.ji и соответствующие файлы .so, .dll или .dylib) для данного модуля. Для этого при компиляции хэш всех предпочтений времени компиляции сериализуется, а затем при поиске нужных файлов проверяется на соответствие текущей среде.

Для предпочтений можно задать значения по умолчанию на уровне хранилища; если пакет Foo устанавливается в глобальной среде и имеет заданные предпочтения, они применяются при условии, что глобальная среда включена в LOAD_PATH. Предпочтения, относящиеся к средам выше в стеке сред, переопределяются записями более низкого уровня в пути загрузки вплоть до текущего активного проекта. Это позволяет существовать предпочтениям по умолчанию на уровне проекта, причем при наследовании для активных проектов они могут объединяться или даже полностью перезаписываться. Подробные сведения о том, как разрешить или запретить объединение предпочтений, см. в docstring функции Preferences.set_preferences!().

Заключение

Федеративное управление пакетами и точная воспроизводимость приложений — сложные для реализации, но важные механизмы. Вместе они усложняют систему загрузки пакетов по сравнению с большинством динамических языков, но в то же время обеспечивают масштабируемость и воспроизводимость, которые обычно ассоциируются со статическими языками. Как правило, при использовании встроенного диспетчера пакетов Julia для управления проектами знать, как именно работают эти механизмы, не требуется. Вызов Pkg.add("X") добавляет пакет X в соответствующие файлы проекта и манифеста, выбранные с помощью Pkg.activate("Y"), так что при дальнейших вызовах import X он загружается без дополнительных усилий.