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

8. Артефакты

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

Базовое использование

Артефакты Pkg объявляются в файле Artifacts.toml, который может быть помещен в текущий каталог или корень пакета. В настоящее время Pkg поддерживает скачивание tar-файлов (которые могут быть сжаты) с URL-адреса. Ниже приведен минимальный файл Artifacts.toml, который позволит скачать socrates.tar.gz из github.com. В этом примере определен один артефакт, которому присвоено имя socrates.

# простой файл Artifacts.toml
[socrates]
git-tree-sha1 = "43563e7631a7eafae1f9f8d9d332e3de44ad7239"

    [[socrates.download]]
    url = "https://github.com/staticfloat/small_bin/raw/master/socrates.tar.gz"
    sha256 = "e65d2f13f2085f2c279830e863292312a72930fee5ba3c792b14c33ce5c5cc58"

Если этот файл Artifacts.toml помещен в текущий каталог, то socrates.tar.gz можно скачать, распаковать и использовать вместе с artifact"socrates". Поскольку этот tar-архив содержит папку bin и текстовый файл с именем socrates в этой папке, мы можем получить доступ к содержимому этого файла следующим образом.

using Pkg.Artifacts

rootpath = artifact"socrates"
open(joinpath(rootpath, "bin", "socrates")) do file
    println(read(file, String))
end

Если у вас есть существующий tar-архив, доступ к которому осуществляется с помощью url, к нему также можно обращаться таким же образом. Для создания Artifacts.toml необходимо вычислить два хэша: хэш sha256 скачанного файла и хэш git-tree-sha1 распакованного содержимого. Их можно вычислить следующим образом.

using Tar, Inflate, SHA

filename = "socrates.tar.gz"
println("sha256: ", bytes2hex(open(sha256, filename)))
println("git-tree-sha1: ", Tar.tree_hash(IOBuffer(inflate_gzip(filename))))

Чтобы получить доступ к этому артефакту из созданного пакета, поместите файл Artifacts.toml в корень пакета, рядом с файлом Project.toml. Затем обязательно добавьте Pkg в deps и задайте julia = "1.3" или выше в разделе compat.

Файлы Artifacts.toml

Pkg предоставляет API для работы с артефактами, а также формат файлов TOML для записи использования артефактов в пакетах и для автоматизации скачивания артефактов во время установки пакета. На артефакты всегда можно ссылаться по хэшу содержимого, но доступ к ним обычно осуществляется по имени, привязанному к хэшу содержимого в файле Artifacts.toml, который находится в дереве исходного кода проекта.

Можно использовать альтернативное имя JuliaArtifacts.toml, аналогично тому, как можно использовать JuliaProject.toml и JuliaManifest.toml вместо Project.toml и Manifest.toml, соответственно.

Вот пример файла Artifacts.toml:

# Пример файла Artifacts.toml
[socrates]
git-tree-sha1 = "43563e7631a7eafae1f9f8d9d332e3de44ad7239"
lazy = true

    [[socrates.download]]
    url = "https://github.com/staticfloat/small_bin/raw/master/socrates.tar.gz"
    sha256 = "e65d2f13f2085f2c279830e863292312a72930fee5ba3c792b14c33ce5c5cc58"

    [[socrates.download]]
    url = "https://github.com/staticfloat/small_bin/raw/master/socrates.tar.bz2"
    sha256 = "13fc17b97be41763b02cbb80e9d048302cec3bd3d446c2ed6e8210bddcd3ac76"

[[c_simple]]
arch = "x86_64"
git-tree-sha1 = "4bdf4556050cb55b67b211d4e78009aaec378cbc"
libc = "musl"
os = "linux"

    [[c_simple.download]]
    sha256 = "411d6befd49942826ea1e59041bddf7dbb72fb871bb03165bf4e164b13ab5130"
    url = "https://github.com/JuliaBinaryWrappers/c_simple_jll.jl/releases/download/c_simple+v1.2.3+0/c_simple.v1.2.3.x86_64-linux-musl.tar.gz"

[[c_simple]]
arch = "x86_64"
git-tree-sha1 = "51264dbc770cd38aeb15f93536c29dc38c727e4c"
os = "macos"

    [[c_simple.download]]
    sha256 = "6c17d9e1dc95ba86ec7462637824afe7a25b8509cc51453f0eb86eda03ed4dc3"
    url = "https://github.com/JuliaBinaryWrappers/c_simple_jll.jl/releases/download/c_simple+v1.2.3+0/c_simple.v1.2.3.x86_64-apple-darwin14.tar.gz"

[processed_output]
git-tree-sha1 = "1c223e66f1a8e0fae1f9fcb9d3f2e3ce48a82200"

Этот файл Artifacts.toml привязывает три артефакта: один с именем socrates, один с именем c_simple и один с именем processed_output. Единственным обязательным элементом информации для артефакта является его git-tree-sha1. Поскольку артефакты рассматриваются только по хэшу содержимого, назначение файла Artifacts.toml — предоставить метаданные об этих артефактах, например связать удобное для восприятия человеком имя с хэшем содержимого, предоставить информацию о том, откуда можно скачать артефакт, или даже связать одно имя с несколькими хэшами, привязанными к характерным для платформы ограничениям, таким как операционная система или версия libgfortran.

Типы и свойства артефактов

В приведенном выше примере артефакт socrates демонстрирует платформонезависимый артефакт с несколькими местами скачивания. При загрузке и установке артефакта socrates URL-адреса будут перебираться по порядку до тех пор, пока не будет найден нужный. Артефакт socrates помечен как lazy, что означает, что он не будет автоматически скачан при установке содержащего его пакета, а будет скачан по требованию, когда пакет впервые попытается его использовать.

Артефакт c_simple демонстрирует платформозависимый артефакт, где каждая запись в массиве c_simple содержит ключи, которые помогают вызывающему пакету выбрать подходящие файлы для скачивания на основе особенностей хост-компьютера. Обратите внимание, что каждый артефакт содержит как git-tree-sha1, так и sha256 для каждой записи скачивания. Это необходимо для того, чтобы убедиться в безопасности загруженного tar-архива перед попыткой его распаковки, а также для того, чтобы гарантировать, что все tar-архивы должны расширяться до одного и того же общего хэша дерева.

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

Использование артефактов

Для работы с артефактами можно использовать вспомогательные API, предоставляемые из пространства имен Pkg.Artifacts. В качестве мотивирующего примера представим, что мы пишем пакет, который должен загрузить набор данных машинного обучения «Ирис». Хотя можно было бы просто скачать набор данных на этапе сборки в каталог пакета и многие пакеты сейчас именно так и поступают, этот способ имеет ряд существенных недостатков.

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

  • Во-вторых, скачанные данные не распространяются между разными версиями пакета. Если у нас есть три разные версии пакета, установленные для использования в разных проектах, то нам нужны три разные копии данных, даже если они не изменились между этими версиями. Более того, каждый раз, когда мы обновляем пакет или понижаем его версию, если только мы не делаем что-то продуманное (и, вероятно, ненадежное), нам приходится скачивать данные снова.

Используя артефакты, вместо этих действий мы проверим, существует ли артефакт iris на диске, и только если его там нет, скачаем и установим его, после чего сможем привязать результат к файлу Artifacts.toml:

using Pkg.Artifacts

# Это путь к файлу Artifacts.toml, с которым мы будем работать
artifact_toml = joinpath(@__DIR__, "Artifacts.toml")

# Запросим файл `Artifacts.toml` для хэша, связанного с именем iris
# (возвращает `nothing`, если такая привязка отсутствует)
iris_hash = artifact_hash("iris", artifact_toml)

# Если имя не было привязано или хэш, к которому оно было привязано, не существует, создадим его.
if iris_hash == nothing || !artifact_exists(iris_hash)
    # create_artifact() возвращает хэш содержимого каталога артефакта после того, как мы закончили его создание
    iris_hash = create_artifact() do artifact_dir
        # Мы создаем артефакт, просто скачав несколько файлов в новый каталог артефактов
        iris_url_base = "https://archive.ics.uci.edu/ml/machine-learning-databases/iris"
        download("$(iris_url_base)/iris.data", joinpath(artifact_dir, "iris.csv"))
        download("$(iris_url_base)/bezdekIris.data", joinpath(artifact_dir, "bezdekIris.csv"))
        download("$(iris_url_base)/iris.names", joinpath(artifact_dir, "iris.names"))
    end

    # Теперь привяжем этот хэш в файле `Artifacts.toml`.  `force = true` означает, что если он уже существует,
    # он просто перезаписывается новым хэшем содержимого.  Если исходные файлы не меняются, мы не ожидаем
    # изменения хэша содержимого, так что ненужная работа с управлением версиями не потребуется.
    bind_artifact!(artifact_toml, "iris", iris_hash)
end

# Получим путь к набору данных iris, созданному только что или ранее.
# это должно быть что-то вроде `~/.julia/artifacts/dbd04e28be047a54fbe9bf67e934be5b5e0d357a`.
iris_dataset_path = artifact_path(iris_hash)

Для конкретного случая использования артефактов, которые были ранее связаны, существует сокращение artifact"name", которое автоматически ищет файл Artifacts.toml, содержащийся в текущем пакете, ищет заданный артефакт по имени, устанавливает его, если он еще не установлен, а затем возвращает путь к этому артефакту. Пример этого сокращения приведен ниже.

using Pkg.Artifacts

# Чтобы это сработало, файл `Artifacts.toml` должен находиться в текущем рабочем каталоге
# (или в корне текущего пакета) и должен определять отображение для артефакта
# iris.  Если его нет на диске, он будет скачан.
iris_dataset_path = artifact"iris"

API Pkg.Artifacts

API Artifacts разбит на три уровня: функции с поддержкой хэша, функции с поддержкой имен и служебные функции.

  • Функции с поддержкой хэша работают с хэшами содержимого и, по сути, ни с чем другим. Эти методы позволяют запрашивать, существует ли артефакт, каков его путь, проверять, удовлетворяет ли артефакт хэшу содержимого на диске, и т. д. К функциям с поддержкой хэша относятся: artifact_exists(), artifact_path(), remove_artifact(), verify_artifact() и archive_artifact(). Обратите внимание, что в общем случае не следует использовать remove_artifact(), а следует использовать Pkg.gc() для очистки установок артефактов.

  • Функции с поддержкой имен работают со связанными именами в файле Artifacts.toml и поэтому обычно требуют как путь к файлу Artifacts.toml, так и имя артефакта. К функциям с поддержкой имен относятся: artifact_meta(), artifact_hash(), bind_artifact!(), unbind_artifact!(), download_artifact() и ensure_artifact_installed().

  • Служебные функции предназначены для различных действий с артефактами, такими как create_artifact(), ensure_all_artifacts_installed() и даже строковой макрос @artifact_str.

Полный список docstrings и методов приведен в разделе Справочник по артефактам.

Переопределение расположений артефактов

Иногда требуется переопределить расположение и содержимое артефакта. Распространенным случаем является вычислительная среда, в которой должны использоваться определенные версии двоичных зависимостей, независимо от того, с какой версией этой зависимости был опубликован пакет. Хотя стандартная конфигурация Julia скачивает, распаковывает и соединяет с общей библиотекой, системному администратору может понадобиться отключить эти возможности и вместо них использовать библиотеку, уже установленную на локальном компьютере. Для этого Pkg поддерживает файл Overrides.toml для каждого хранилища, размещаемый в каталоге хранилища artifacts (например, ~/.julia/artifacts/Overrides.toml для пользовательского хранилища по умолчанию), который может переопределять расположение артефакта либо по хэшу содержимого, либо по UUID пакета и привязанному имени артефакта. Кроме того, местом назначения может быть либо абсолютный путь, либо хэш содержимого заменяющего артефакта. Это позволяет системным администраторам создавать собственные артефакты, которые они могут использовать, переопределяя другие пакеты для применения нового артефакта.

# Переопределим хэш на абсолютный путь
78f35e74ff113f02274ce60dab6e92b4546ef806 = "/path/to/replacement"

# Переопределим хэш на хэш содержимого нового артефакта
683942669b4639019be7631caa28c38f3e1924fe = "d826e316b6c0d29d9ad0875af6ca63bf67ed38c3"

# Переопределим привязки пакетов, указав UUID пакета и имя привязанного артефакта
# Мы предполагаем, что в демонстрационных целях этот пакет называется `Foo`
[d57dbccd-ca19-4d82-b9b8-9d660942965b]
libfoo = "/path/to/libfoo"
libbar = "683942669b4639019be7631caa28c38f3e1924fe"

В связи с многоуровневым характером хранилищ Pkg одновременно могут быть задействованы несколько файлов Overrides.toml. Это позволяет «внутренним» файлам Overrides.toml переопределять переопределения, размещенные во «внешних» файлах Overrides.toml. Чтобы удалить переопределение и снова включить логику расположения по умолчанию для артефакта, вставьте запись, сопоставляющую пустую строку:

78f35e74ff113f02274ce60dab6e92b4546ef806 = "/path/to/new/replacement"
683942669b4639019be7631caa28c38f3e1924fe = ""

[d57dbccd-ca19-4d82-b9b8-9d660942965b]
libfoo = ""

Если два фрагмента Overrides.toml, приведенные выше, наложить друг на друга, конечным результатом будет сопоставление хэша содержимого 78f35e74ff113f02274ce60dab6e92b4546ef806 с "/path/to/new/replacement" и сопоставление Foo.libbar с артефактом, идентифицируемым хэшем содержимого 683942669b4639019be7631caa28c38f3e1924fe. Обратите внимание, что если раньше этот хэш был переопределен, то теперь это не так, и поэтому Foo.libbar будет обращаться непосредственно к таким расположениям, как ~/.julia/artifacts/683942669b4639019be7631caa28c38f3e1924fe.

Большинство методов, затрагиваемых переопределениями, могут игнорировать переопределения, если для них задано honor_overrides=false в качестве именованного аргумента. Для работы переопределений на основе UUID или имени файлы Artifacts.toml должны быть загружены с учетом UUID загружаемого пакета. Это автоматически определяет строковой макрос artifacts"", однако, если вы по какой-то причине вручную используете API Pkg.Artifacts в своем пакете и хотите учитывать переопределения, необходимо предоставить UUID пакета таким вызовам API, как artifact_meta() и ensure_artifact_installed(), с помощью именованного аргумента pkg_uuid.

Выбор расширенных платформ

Совместимость: Julia 1.7

Для выбора расширенных платформ Pkg требуется версия не ниже Julia 1.7. Эта функция считается экспериментальной.

Новое в Julia 1.6: к объектам Platform могут быть применены расширенные атрибуты, что позволяет добавлять к артефактам такие метки, как совместимость с версией драйвера CUDA, совместимость микроархитектур, совместимость с версией Julia и проч. Обратите внимание, что эта функция считается экспериментальной и может измениться в будущем. Если вы, как разработчик пакетов, сочтете эту функцию необходимой, свяжитесь с нами, чтобы мы смогли доработать ее на благо всей экосистемы. Для поддержки выбора артефакта во время Pkg.add() Pkg запустит специально именованный файл <project_root>/.pkg/select_artifacts.jl, передав в качестве первого аргумента триплет текущей платформы. Этот скрипт выбора артефактов должен вывести TOML-сериализованный словарь, представляющий артефакты, которые нужны этому пакету в соответствии с заданной платформой, и выполнить любую проверку системы, необходимую для автоматического определения возможностей платформы, если триплет данной платформы не предоставляет их явным образом. Формат словаря должен соответствовать формату, возвращаемому из Artifacts.select_downloadable_artifacts(), и действительно, большинство пакетов должны просто вызывать эту функцию с дополненным объектом Platform. Пример определения обработчика выбора артефакта может выглядеть следующим образом с разделением между двумя файлами:

# .pkg/platform_augmentation.jl
using Libdl, Base.BinaryPlatforms
function augment_platform!(p::Platform)
    # Если для этого объекта платформы задан тег `cuda`, не будем дополнять его
    if haskey(p, "cuda")
        return p
    end

    # Откроем libcuda явным образом, чтобы она получила `dlclose()` после того, как мы закончим
    dlopen("libcuda") do lib
        # найдем символ для запроса версии драйвера; если мы не можем его найти, просто продолжаем
        cuDriverGetVersion = dlsym(lib, "cuDriverGetVersion"; throw_error=false)
        if cuDriverGetVersion !== nothing
            # Запросим версию драйвера CUDA:
            driverVersion = Ref{Cint}()
            ccall(cuDriverGetVersion, UInt32, (Ptr{Cint},), driverVersion)

            # Сохраним только номер основной версии
            p["cuda"] = div(driverVersion, 1000)
        end
    end

    # Возвратим возможно измененный объект `Platform`
    return p
end
using TOML, Artifacts, Base.BinaryPlatforms
include("./platform_augmentation.jl")
artifacts_toml = joinpath(dirname(@__DIR__), "Artifacts.toml")

# Получим «целевой триплет» из ARGS, если он задан (в противном случае по умолчанию используется ведущий триплет).
target_triplet = get(ARGS, 1, Base.BinaryPlatforms.host_triplet())

# Дополним этот объект платформы специальными тегами, которые нам нужны
platform = augment_platform!(HostPlatform(parse(Platform, target_triplet)))

# Выберем все скачиваемые артефакты, соответствующие данной платформе
artifacts = select_downloadable_artifacts(artifacts_toml; platform)

# Выведем результат в `stdout` в виде словаря TOML
TOML.print(stdout, artifacts)

В этом определении обработчика процедура расширения платформы открывает системную библиотеку (libcuda), ищет в ней символ, который выдает версию драйвера CUDA, а затем внедряет номер основной версии в свойство cuda объекта Platform, который мы расширяем. Хотя этот код не обязательно должен закрывать загруженную библиотеку (поскольку она, скорее всего, будет снова открыта пакетом CUDA сразу после завершения операций с пакетом), лучше всего сделать обработчики как можно более простыми и прозрачными, поскольку в будущем их могут использовать другие служебные функции Pkg. В своем пакете при применении макроса @artifact_str также следует использовать объекты расширенной платформы, как показано ниже:

include("../.pkg/platform_augmentation.jl")

function __init__()
    p = augment_platform!(HostPlatform())
    global my_artifact_dir = @artifact_str("MyArtifact", p)
end

Это гарантирует, что в вашем коде будет использоваться тот же артефакт, который пытался установить Pkg.

Обработчики выбора артефактов могут использовать только Base, Artifacts, Libdl и TOML. Они не могут использовать другие стандартные библиотеки, а также пакеты (включая пакет, которому они принадлежат).