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

Хорошо, признаю, когда я читал Документацию по Julia Artifacts, я был очень запутан во многих вещах. Если вы один из тех, кто умел в этом разобраться, то поздравляю, вам не нужно читать дальше.

Остальным из нас нужно немного больше введения и контекста для того, что такое Артефакты, а также более подробное пошаговое руководство по API и использованию. Это то, что я надеюсь предоставить здесь. Я также расскажу о своих первоначальных заблуждениях в надежде, что у вас могут быть такие же, и я смогу разъяснить вам некоторые из них.

Прежде чем мы начнем, позвольте мне вкратце намекнуть на то, о чем мы говорим. Артефакты в Julia существуют в виде модуля внутри модуля Pkg под названием Pkg.Artifacts. Вы получаете доступ к функциям REPL через:

julia> using Pkg.Artifacts

Он предлагает такие функции, как artifact_toml, artifact_path и artifact_exists. Мы рассмотрим их позже.

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

Foobar
├── Artifacts.toml        # Stores info about artifacts
├── Manifest.toml
├── Project.toml          # Direct package dependencies
├── src
│   └── Foobar.jl         # Defines Foobar module
└── test
    ├── Manifest.toml
    ├── Project.toml       # Package dependencies for tests
    ├── runtests.jl        # Contains your tests

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

Разве мы не можем просто поместить все данные в наш репозиторий Git?

Если бы размещение изображений, двоичных файлов, наборов данных и подобных данных в репозиториях git было безболезненным и беспроблемным, нам, возможно, вообще не понадобились бы артефакты.

Однако git не создан для двоичных данных. Таким образом, размещение такого рода данных в репозитории исходного кода легко раздувает его. Вторая проблема заключается в том, что для двоичных файлов требования к пространству могут быстро стать безумными. Представьте, что у вас есть зависимость от чего-то вроде библиотеки Qt, OpenCV, VTK или любого другого монстра-проекта C ++. Компиляция версии для каждой платформы, 32-битной, 64-битной и множества других вариаций и включение ее в вашу библиотеку исходного кода, приведет к катастрофе. Это заняло бы слишком много места.

Проблемы, решаемые артефактами

С помощью Artifacts несколько пакетов в принципе могут использовать одни и те же данные, и вам не нужно загружать их дважды. Скажем, пакет A и B, оба зависят от библиотеки Qt. Фиктивное решение этого состоит в том, чтобы обе библиотеки хранили копию Qt:

A
└─ libs
    └── Qt 

B
└─ libs
    └── Qt

Но теперь нам нужно дважды загрузить огромную библиотеку, и мы занимаем вдвое больше места на жестком диске. Так что не лучшее решение. Теперь кто-то другой может подумать, что он умен, и сохранить Qt в общем каталоге для использования обоими пакетами. Различные операционные системы сделали это на раннем этапе и создали забавную вещь, которую мы называем «адом DLL». Это происходит, когда требуемая версия Qt не совсем та. Загружаемая версия Qt может работать для A, но не для B.

Адресация данных по содержанию

Git популяризировал решение этой головоломки, называемое данными с адресацией по содержанию. Это означает, что данные можно искать не по путям имен, например A/libs/Qt, а по забавным путям, например:

7c9ef733699a1d86b8a6073ed08a4457e3e790f7/Qt

Какого черта? Для чего нужны все эти уродливые числа? Это хэш библиотеки Qt, хранящейся внутри. Хэш - это строка фиксированной длины, полученная путем запуска алгоритма по всем байтам, составляющим некоторые данные. В этом случае каждый байт двоичных файлов библиотеки Qt загружается в алгоритм хеширования, и он создает уникальный номер - хеш. Теоретически вы, конечно, не можете гарантировать, что два набора данных производят разные хэши. Это действительно основано на вероятности. Вероятность того, что разные наборы данных дадут один и тот же хэш, аналогична тому, что два человека в случайных местах на земле подберут одну и ту же песчинку. Это могло случиться, но маловероятно.

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

$ cd ~/.julia
$ ls
artifacts/    compiled/     config/       environments/ packages/     registries/
clones/       conda/        dev/          logs/         prefs/

Здесь следует отметить, что артефакты хранятся не в пакетах, которым они принадлежат, а в каталоге artifacts:

> ls ~/.julia/artifacts/
0c063b1aef124b99659e1604675f2dce871e7e97
0eb6ffcbe0cde10007a2678013611da80643b728
6b04ad7f71b1a29adad801f4fdd0d34b3535c638

Мы можем заглянуть внутрь некоторых из них, чтобы увидеть, что там:

$ ls ~/.julia/artifacts/6b04ad7f71b1a29adad801f4fdd0d34b3535c638
bin include lib logs    share

$ ls ~/.julia/artifacts/6b04ad7f71b1a29adad801f4fdd0d34b3535c638/lib/
engines-1.1     libcrypto.a     libssl.1.1.dylib    libssl.dylib
libcrypto.1.1.dylib libcrypto.dylib     libssl.a        pkgconfig

Как видите, это криптографическая библиотека, от которой зависит какой-то пакет. Каждый пакет, зависящий от этой конкретной криптобиблиотеки, будет иметь Artifacts.toml файл, в котором говорится, что криптобиблиотека имеет хэш 6b04ad7f71b1a29adad801f4fdd0d34b3535c638. Затем система пакетов Julia может проверить, установлена ​​ли уже эта библиотека, проверив, существует ли каталог с этим хешем, и избежать загрузки той же библиотеки во второй раз.

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

Избегайте раздувания репозитория Git

Вторая проблема с артефактами заключается в том, что они избегают перегрузки вашего репозитория двоичными файлами. Допустим, у вас есть Comics пакет с кодом для показа комиксов. Вместо того, чтобы помещать каталог super-heroes в репозиторий пакетов с изображениями каждого супергероя, вы создаете файл Artifacts.toml. В нем вы описываете, где находятся изображения супергероев:

# Comics/Artifacts.toml 
[super-heroes]
git-tree-sha1 = "c5f4d31e5c9c5d6fba2469ceff3d9916050d92d2"
lazy = true

    [[super-heroes.download]]
    sha256 = "2aea399ab3c6b6e3a4285ec6ae31b858803442bf1b3e3e4889a2e3e8287d56c6"
    url = "https://github.com/jimmy/Comics.jl/releases/download/super-heroes.tar.gz"

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

path = joinpath("super-heroes", "batman.png")
# equivalent to: path = "super-heroes/batman.png"
batman_img = load(path)

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

using Pkg.Artifacts

path = joinpath(artifact"super-heroes", "batman.png")
batman_img = load(path)

Часть artifact"super-heroes" - это макрос. Это заставляет Джулия запускать код во время синтаксического анализа, который загружает артефакт. Если lazy = true в Artifacts.toml файле, то артефакт будет загружен только при таком доступе. Это позволяет избежать загрузки большего количества, чем фактически использует пользователь.

Во время выполнения выражение artifact"super-heroes" возвращает локальный путь к артефакту, поэтому вы можете его загрузить.

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

julia> super_hash = artifact_hash("super-heroes", "Artifacts.toml")
SHA1("c5f4d31e5c9c5d6fba2469ceff3d9916050d92d2")

julia> basepath = artifact_path(super_hash)
"~/.julia/artifacts/c5f4d31e5c9c5d6fba2469ceff3d9916050d92d2"

julia> path = joinpath(basepath, "batman.png")
"~.julia/artifacts/c5f4d31e5c9c5d6fba2469ceff3d9916050d92d2/batman.png"

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

$ tree Comics
Comics/
└── Artifacts.toml

Различные функции артефактов на самом деле просто читают файл Artifacts.toml. Конечно, поскольку мы не помещали никаких .tar.gz файлов в указанный URL и не помещали ничего в каталог ~/.julia/artifacts, мы, конечно, не сможем ничего загрузить.

Создание файла Artifacts.toml

Создание файла Artifacts.toml - вот что меня сначала сильно смутило. Я не мог понять:

  1. Откуда берется хеш SHA1 в git-tree-sha1? Мне нужно запустить какую-то команду для его создания?
  2. Откуда берется хеш sha256 и почему два хеша?

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

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

Мое первоначальное заблуждение об этих сценариях состоит в том, что я думал, что они запускаются во время установки пакета. Однако эти сценарии запускаются создателем пакета. Им даже не нужно быть частью пакета. Это ваш выбор. Это просто удобно.

git-tree-sha1

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

julia> create_artifact() do dir
           @info "storage dir: " dir
       end
┌ Info: storage dir:
└   dir = "~/.julia/artifacts/jl_l4XQEX"
SHA1("4b825dc642cb6eb9a060e54bf8d69288fbee4904")

Вы можете видеть, что он создает временный каталог с именем jl_l4XQEX для размещения файлов, которые вы хотите хэшировать. Если каталог пуст, вы можете увидеть, что мы получили хэш "4b825dc642cb6eb9a060e54bf8d69288fbee4904". Конечно, это бессмысленно. Мы хотим поместить в него несколько реальных файлов. Сделаем пару файлов.

$ mkdir data
$ echo "hello world" > data/hello.txt
$ echo "how do you do?" > data/howdy.txt

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

julia> hash = create_artifact() do dir
           cp("data/hello.txt", joinpath(dir, "hello.txt"))
           cp("data/howdy.txt", joinpath(dir, "howdy.txt"))
       end
SHA1("604c1a4760bd32fbb9758a84c1507a117252e688")

На этом этапе вы можете увидеть, что файлы были сохранены под правильным хешем артефакта:

julia> artifact_path(hash)
"~/.julia/artifacts/604c1a4760bd32fbb9758a84c1507a117252e688"

julia> readdir(artifact_path(hash))
2-element Array{String,1}:
 "hello.txt"
 "howdy.txt"

Хэш SHA256 tarball

Затем мы хотим каким-то образом загрузить эти файлы. В принципе, их можно упаковать любым способом, и мы хотим убедиться, что загруженные файлы .zip, .tar или .tar.gz не были подделаны. Таким образом, мы берем хэш сжатого tar-архива, поскольку в принципе он может быть чем-то совершенно отличным от дерева Git SHA1, которое относится к хешу распакованных файлов.

julia> tarball_hash = archive_artifact(hash, "data.tar.gz")
"7bd1483bc31fd3cd885a6b3c8f953af5815ac7729593bde29c6cdec9ca16268e"

Функция archive_artifact берет содержимое каталога артефактов, связанного с заданным хешем, и создает сжатый архив по заданному пути к имени файла. Он выполняет регулярное хеширование SHA256 полученного архива. Вы можете убедиться, что в оболочке:

shell> ls
Artifacts.toml  data        data.tar.gz

shell> shasum -a 256 data.tar.gz
7bd1483bc31fd3cd885a6b3c8f953af5815ac7729593bde29c6cdec9ca16268e  data.tar.gz

Мы также можем перечислить содержимое архива, чтобы убедиться, что он действительно содержит два файла, хранящихся в каталоге артефактов.

shell> tar -tf data.tar.gz
./
./howdy.txt
./hello.txt

Запись подробностей в Artifacts.toml

Теперь, когда у нас есть все хэши, мы можем в принципе ввести соответствующую информацию в файл Artifacts.toml. Вам также понадобится URL-адрес, по которому вы хотите сохранить файл data.tar.gz.

julia> bind_artifact!("Artifacts.toml", "hi", hash,
          download_info=[("http://howdy.com/data.tar.gz", 
                          tarball_hash)])

Это помещает артефакт с именем hi в файл Artifacts.toml с хешем SHA1 hash, который однозначно идентифицирует этот артефакт. Далее он указывает, что пользователи пакета будут загружать этот hi артефакт с http://howdy.com/data.tar.gz URL. Он также записывает хэш SHA256 для этих загруженных файлов как tarball_hash. Таким образом, когда пользователь загружает этот файл, система может проверить, что у него такой же хэш.

Давайте посмотрим, как сейчас выглядит Artifacts.toml:

shell> cat Artifacts.toml
[hi]
git-tree-sha1 = "604c1a4760bd32fbb9758a84c1507a117252e688"

    [[hi.download]]
    sha256 = "7bd1483bc31fd3cd885a6b3c8f953af5815ac7729593bde29c6cdec9ca16268e"
    url = "http://howdy.com/data.tar.gz"

[super-heroes]
git-tree-sha1 = "c5f4d31e5c9c5d6fba2469ceff3d9916050d92d2"
lazy = true

    [[super-heroes.download]]
    sha256 = "2aea399ab3c6b6e3a4285ec6ae31b858803442bf1b3e3e4889a2e3e8287d56c6"
    url = "https://github.com/jimmy/Comics.jl/releases/download/super-heroes.tar.gz"

Тот, кто хочет получить эти данные, теперь может использовать:

julia> hi_hash = artifact_hash("hi", "Artifacts.toml")
SHA1("604c1a4760bd32fbb9758a84c1507a117252e688")

julia> artifact_path(hi_hash)
"~/.julia/artifacts/604c1a4760bd32fbb9758a84c1507a117252e688"

Конечно, можно использовать ярлык artifact"hi", но при этом будет предпринята попытка загрузки с http://howdy.com/data.tar.gz, а у меня там ничего не хранится.

Если вы хотите просто проверить, как работает эта загрузка, вы можете просто указать URL-адрес, указывающий на локальное расположение файла. Поскольку вы изменяете то, что записано в файле Artifacts.toml, вам необходимо указать force=true.

julia> bind_artifact!("Artifacts.toml", "hi", hash, download_info=[("file:/home/steve/dev/Comics/data.tar.gz", tarball_hash)], force=true)
shell> cat Artifacts.toml
[hi]
git-tree-sha1 = "604c1a4760bd32fbb9758a84c1507a117252e688"
[[hi.download]]
    sha256 = "7bd1483bc31fd3cd885a6b3c8f953af5815ac7729593bde29c6cdec9ca16268e"
    url = "file:/home/steve/dev/Comics/data.tar.gz"

В этом примере мы делаем вид, что ваш пакет Comics хранится в /home/steve/dev. Замените его действительным путем на жестком диске, и тогда artifact"hi" вызовет «загрузку» из этого места при первом доступе.

Примеры артефактов

Если вы хотите увидеть, как все это собрано вместе для создания реальных сценариев артефактов, вы можете посмотреть на использование артефактов в пакете ObjectDetector.jl.

Также в официальной документации есть этот пример:

using Pkg.Artifacts

artifact_toml = joinpath(@__DIR__, "Artifacts.toml")
iris_hash = artifact_hash("iris", artifact_toml)

if iris_hash == nothing || !artifact_exists(iris_hash)
     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

    bind_artifact!(artifact_toml, "iris", iris_hash)
end

iris_dataset_path = artifact_path(iris_hash)

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

Работа с двоичными файлами

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

Об этом я расскажу в моем следующем рассказе: Бинарные артефакты Джулии для чайников.

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