Code Loading

Note

Эта глава охватывает технические детали загрузки пакетов. Чтобы установить пакеты, используйте Pkg, встроенный менеджер пакетов Julia, чтобы добавить пакеты в вашу активную среду. Чтобы использовать пакеты, уже находящиеся в вашей активной среде, напишите import X или using X, как описано в Modules documentation.

Definitions

У Джулии есть два механизма для загрузки кода:

  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 внутри импортирующего модуля. Если один и тот же пакет X импортируется несколько раз в одной и той же сессии Julia, он загружается только в первый раз — при последующих импортах импортирующий модуль получает ссылку на тот же модуль. Однако стоит отметить, что import X может загружать разные пакеты в разных контекстах: X может относиться к одному пакету с именем X в основном проекте, но потенциально к разным пакетам, также названным X, в каждой зависимости. Подробнее об этом ниже.

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

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

Эти вопросы отвечаются путем поиска в средах проекта, перечисленных в LOAD_PATH, для файлов проекта (Project.toml или JuliaProject.toml), файлов манифеста (Manifest.toml или JuliaManifest.toml, или тех же имен с суффиксом -v{major}.{minor}.toml для конкретных версий) или папок исходных файлов.

Federation of packages

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

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

Одним из последствий федерации является то, что не может быть центрального органа для именования пакетов. Разные сущности могут использовать одно и то же имя для обозначения несвязанных пакетов. Эта возможность неизбежна, поскольку эти сущности не координируют свои действия и могут даже не знать друг о друге. Из-за отсутствия центрального органа именования один проект может в конечном итоге зависеть от разных пакетов, которые имеют одно и то же имя. Механизм загрузки пакетов в Julia не требует, чтобы имена пакетов были глобально уникальными, даже в пределах графа зависимостей одного проекта. Вместо этого пакеты идентифицируются с помощью universally unique identifiers (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). Как работает это различие, определяется окружениями, как объясняется в следующих разделах.

Environments

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

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

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

Эти среды служат разным целям:

  • Проектные окружения обеспечивают воспроизводимость. Проверяя проектное окружение в систему контроля версий — например, в репозиторий git — вместе с остальным исходным кодом проекта, вы можете воспроизвести точное состояние проекта и всех его зависимостей. Файл манифеста, в частности, фиксирует точную версию каждой зависимости, идентифицированной с помощью криптографического хеша ее исходного дерева, что позволяет Pkg извлекать правильные версии и быть уверенным, что вы запускаете именно тот код, который был зафиксирован для всех зависимостей.
  • Каталоги пакетов обеспечивают удобство, когда полная тщательно отслеживаемая среда проекта не требуется. Они полезны, когда вы хотите разместить набор пакетов где-то и иметь возможность использовать их напрямую, не создавая для них среду проекта.
  • Сложенные окружения позволяют добавлять инструменты в основное окружение. Вы можете добавить окружение инструментов разработки в конец стека, чтобы сделать их доступными из REPL и скриптов, но не изнутри пакетов.

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

  • корни: name::Symboluuid::UUID

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

  • граф: context::UUIDname::Symboluuid::UUID

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

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

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

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

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 признается как формат, позволяющий конкретной версии julia использовать определенный файл манифеста, т.е. в одной и той же папке 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, UUID пакета Priv, который должен быть загружен в этом контексте. Этот UUID определяет, какой пакет Priv загружать и использовать, когда основное приложение выполняет import 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, которые использует приложение. Оно использует частный пакет, который является корневой зависимостью, и публичный, который является косвенной зависимостью через Pub. Эти пакеты различаются по своим уникальным идентификаторам (UUID), и у них разные зависимости:

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

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

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(),
)

Учитывая этот граф зависимости, когда 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 может относиться к разным пакетам в основном проекте, чем в одном из зависимостей его пакета, что позволяет иметь дублирующиеся имена в экосистеме пакетов.

Что произойдет, если в основном коде App будет выполнен import Zebra? Поскольку 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 — и ищите каталог с именем packages/X/$slug в каждом каталоге глобального массива Julia DEPOT_PATH. Используйте первый такой каталог, который существует.

Если любой из этих результатов приведет к успеху, путь к точке входа исходного кода будет либо этот результат, либо относительный путь от этого результата плюс 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 — она находит его строфу в манифесте, видит, что у него нет записи path, но есть запись git-tree-sha1. Затем она вычисляет slug для этой пары UUID/SHA-1, который равен 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

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

Вот представление возможной карты путей для нашего примера проекта 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",
)

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

  1. Пакет Priv является "vendored" внутри репозитория App.
  2. Публичные пакеты Priv и Zebra находятся в системном депо, где хранятся пакеты, установленные и управляемые системным администратором. Эти пакеты доступны всем пользователям системы.
  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, uuid — это фиктивный UUID, сгенерированный путем хеширования канонического (реального) пути к X/Project.toml.
  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, назначенные этим пакетам, строго внутренние.

Наблюдайте за следующими конкретными примерами этих правил в нашем примере:

  • 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 всегда имеют эту форму.

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

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

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

Package Extensions

Пакет "расширение" — это модуль, который автоматически загружается, когда в текущей сессии 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 — это названия расширений. Они загружаются, когда загружены все пакеты с правой стороны (триггеры) этого расширения. Если у расширения только один триггер, список триггеров можно записать просто как строку для краткости. Местоположение точки входа расширения находится либо в 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

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

Package/Environment Preferences

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

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

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

Conclusion

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