Манифесты

From docs/ru/MANIFESTS.md at 4e43843 View on GitHub

Манифест — это конфигурация одного пакета: откуда он берётся, какой ассет релиза брать и что выставлять наружу после распаковки.

Он нужен редко. ketch выводит манифест из одного лишь owner/repo, и для большинства проектов этого достаточно. Пишите манифест, когда вывод ошибается: ассет с необычным именем, бинарник, на который нужно поставить ссылку под другим именем, бандл .app, короткий алиас, который стоит запомнить.

Одна и та же схема используется в трёх местах:

ГдеФайлДля кого
Ваша машина~/.ketch/manifests/<name>.tomlтолько для вас
Реестр пакетов<package>/ketch.tomlдля всех — см. REGISTRY.md
Встроено в ketchcrates/ketch-core/src/builtin.tomlдля всех, офлайн

Поиск идёт в этом порядке, поэтому ваш собственный манифест всегда побеждает.

Как написать манифест

ketch config create напишет его за вас, так что схема ниже — справочник, который можно читать, а не форма, которую надо заучивать. Команда спрашивает, что должно быть в каждом поле, — источник, имя, записи bin, шаблоны ассетов и остальное, — показывая значение по умолчанию, чтобы пустой ответ его сохранял, а затем показывает собранный файл перед записью.

ketch config create    # спрашивает, показывает, записывает ./ketch.toml

--file записывает в другое место, --force заменяет уже существующий файл, а --yes записывает без финального подтверждения. Ответы можно вместо этого передать через stdin — по одному на строку, пустая строка означает значение по умолчанию, — так что опросник работает как из скрипта, так и в терминале. Дальше, чтобы предложить файл всем, нужен пул-реквест в реестр — см. REGISTRY.md.

Если пакет уже есть в winget, Homebrew или Arch Linux, ketch import записывает манифест из этого описания, оставляя только поля ниже, которые нужны ketch — см. ketch import.

ketch import brew codex --dry-run   # напечатать ~/.ketch/manifests/codex.toml

Поддержка редактора

manifest.schema.json — это JSON Schema файла ketch.toml, сгенерированная из типов, которыми ketch его читает. Сервер языка TOML, понимающий директиву #:schema — Taplo, на котором работает расширение Even Better TOML, или Tombi — тогда дополняет ключи, показывает документацию ниже по мере набора и отмечает опечатку в ключе раньше, чем это сделает ketch. Поставьте директиву первой строкой:

#:schema https://raw.githubusercontent.com/pyrlyn/ketch/main/docs/manifest.schema.json
source = "github:BurntSushi/ripgrep"

Она описывает один пакет: ketch.toml реестра или проекта либо файл одного пакета в ~/.ketch/manifests/. Файл с массивом [[package]], как builtin.toml, — это список таких пакетов, а не эта форма. name в схеме необязательно, потому что его даёт папка реестра; везде ещё ketch по-прежнему его требует.

Схема говорит то, что схема может: неизвестные ключи, допустимые значения kind, verifier, mode и видов extra_paths, синтаксис source, имена, годные как имя файла, алиасы provides без пробелов, strip_prefix не больше 8, запись bin с name или path и непустые хуки. Остальное из Что проверяет ketch — пути, которые должны оставаться внутри содержимого, как классифицируется запись extra_paths, какие ключи trust принадлежат какому проверяющему и разбирается ли ключ — по-прежнему делает ketch при загрузке. Как и в ketch, записи bin и таблица asset не отклоняют ключи, которых они не знают.

Самый маленький

source = "github:BurntSushi/ripgrep"

У всего остального есть значения по умолчанию. В папке пакета реестра имя берётся из папки; в остальных местах name обязательно. ketch config create также записывает запись bin с именем команды, о котором всегда спрашивает.

Полный

name = "ripgrep"
source = "github:BurntSushi/ripgrep"
description = "Recursively search directories for a regex pattern"
homepage = "https://github.com/BurntSushi/ripgrep"
kind = "binary"
provides = ["rg"]
prerelease = false
strip_prefix = 1
notes = "Shell completions are under complete/ in the payload."

bin = [{ path = "*/rg", name = "rg" }]
extra_paths = ["complete/rg.bash", "doc/rg.1"]

[hooks]
after_install = "./rg --version"
after_update = "rm -rf \"$HOME/.cache/rg\""

[asset]
include = ["*-apple-darwin.tar.gz"]
exclude = ["*-musl-*"]

[asset.target]
"macos-aarch64" = "*-aarch64-apple-darwin.tar.gz"
"macos-x86_64" = "*-x86_64-apple-darwin.tar.gz"
"linux-x86_64" = "*-x86_64-unknown-linux-gnu.tar.gz"
"linux-aarch64" = "*-aarch64-unknown-linux-gnu.tar.gz"
"windows-x86_64" = "*-x86_64-pc-windows-msvc.zip"
"windows-aarch64" = "*-aarch64-pc-windows-msvc.zip"

Поля

КлючТипПо умолчаниюЧто делает
nameстрокаимя папки в пакете реестраИмя установки. Становится каталогом в хранилище и ключом в state.json.
sourceстрока— обязательноowner/repo или scheme:id для плагина источника.
descriptionстроканетОдна строка, которую показывают ketch info и ketch search.
homepageстроканетURL для людей.
kindauto | binary | appautoЧем является содержимое. См. ниже.
providesсписок строкпустоДругие имена, на которые откликается этот пакет. ketch install rg работает, потому что ripgrep предоставляет rg.
prereleaseboolfalseУчитывать пре-релизы при разрешении latest.
strip_prefixцелое 0–8снять один каталог-обёрткуСколько ведущих компонентов пути отбрасывать при распаковке.
notesстроканетПечатается после успешной установки.
binсписок таблицобнаружениеНа какие исполняемые файлы ставить ссылки и под какими именами.
extra_pathsсписок строк или таблицпустоMan-страницы и автодополнения, на которые ставятся ссылки в пользовательских каталогах.
assetтаблицавыбор платформыСужает выбор ассета релиза.
trustтаблицанетЧья подпись должна быть у релиза. См. ниже.
hooksтаблицанетКоманды, выполняемые до и после установки, обновления и удаления. См. ниже.

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

kind

  • auto — посмотреть на содержимое. Бандл .app делает пакет приложением; всё остальное считается бинарниками.
  • binary — ставить ссылки на исполняемые файлы в PATH и никогда не размещать .app.
  • app — разместить бандл .app и не разбрасывать его исполняемые файлы по PATH.

bin

Каждой записи нужен path, name или оба.

bin = [
  { name = "rg" },                    # найти в содержимом файл с именем `rg`
  { path = "bin/tool" },              # поставить ссылку на этот точный путь как `tool`
  { path = "*/tool-*", name = "tool" }, # найти по глобу, поставить ссылку как `tool`
]

path — это глоб (* и ?), сопоставляемый с путём относительно корня содержимого. name — имя файла симлинка в ~/.ketch/bin.

Глоб, которому подходит несколько файлов, ставит ссылку на тот, чей stem равен name (без учёта регистра, .exe игнорируется): rtok* с name = "rtok" берёт rtok, а не rtok-hook. Когда подходят несколько файлов и ни один не назван как name — или у записи нет name — установка отказывается и перечисляет кандидатов, потому что порядок каталога различается между операционными системами и поставил бы ссылку на разный файл. Задайте name или сузьте path до одного файла.

Если bin нет вовсе, ketch сам обнаруживает исполняемые файлы: он смотрит вглубь до четырёх уровней, игнорирует каталоги документации и внутренности бандлов и предпочитает каталог bin/, если он есть в содержимом. Единственный исполняемый файл, имя которого явно несёт метаданные сборки, — jq-macos-arm64, — получает ссылку под именем пакета.

Обнаружение ставит ссылки на всё найденное, пока релиз не содержит несколько исполняемых файлов с именем пакета — скажем, rtok рядом с rtok-hook, где один — это команда, а другой — вспомогательная программа. Имя совпадает с именем пакета, если это имя пакета или имя пакета, за которым следует -, _ или . (gofmt не совпадает с go). Тогда ketch ставит ссылку ровно на один из них, выбранный одинаково на всех ОС, а остальных из этого семейства пропускает; исполняемые файлы с другими именами — other-tool рядом с rtok и rtok-hook — получают ссылки как обычно. Выбор такой:

  1. тот, который назван в --bin <name> у ketch install или ketch upgrade, без учёта регистра и .exe; имя, не совпадающее ни с одним бинарником релиза, — ошибка;
  2. иначе тот, чьё имя совпадает с именем пакета, — rtok, но никогда не rtok-hook, — без учёта регистра и .exe;
  3. иначе тот, который был выбран для этого пакета раньше: state.json помнит его между обновлениями, переустановками, ketch link и ketch rollback, а ketch sync также читает его из ключа bin в ketch.lock (сначала состояние, затем lock-файл);
  4. иначе тот, который вы выберете из нумерованного списка, когда ketch работает в терминале и --yes не передан;
  5. иначе ничего: установка завершается ошибкой со списком кандидатов и записью bin, которая решила бы вопрос.

Если ответивший манифест — ваш собственный (~/.ketch/manifests/<name>.toml), выбор записывается в него как bin = [{ name = "..." }, ...] — выбранный бинарник и все остальные, получившие ссылки, — а остальная часть файла остаётся точно такой, какой была, и с этого момента файл сам называет свои бинарники. Для манифеста из реестра, встроенного или выведенного выбор хранится в state.json (а ketch lock копирует его в ketch.lock), и манифест продолжает обновляться вместе с реестром.

ketch config create всегда спрашивает имя бинарника, поэтому в записанном им файле пакета есть запись bin. Манифест без неё всё равно загружается.

extra_paths

Каждая запись — путь относительно распакованного содержимого, в виде строки или таблицы.

extra_paths = [
  "complete/rg.bash",
  "doc/rg.1",
  { path = "misc/custom", kind = "completion", shell = "bash" },
]

Строка классифицируется по этим правилам и отклоняется, если подходят оба или ни одно:

  • Автодополнение — один из компонентов пути равен complete, completions, completion, bash-completion или site-functions, а имя файла — *.bash, *.zsh, *.fish, *.ps1, *.elv, zsh-шное _name без других точек или имя без расширения внутри bash-completion / site-functions.
  • Man-страница — один из компонентов пути равен man, manN (N — раздел man), doc или docs, а имя файла — name.N или name.N.gz, где N — от 1 до 9, за которым могут следовать строчные буквы (1, 8, 1p).

complete/rg.1 подходит под оба семейства и отклоняется. Файл в корне содержимого, например rg.1, отклоняется. Вместо угадывания задайте kind.

Таблица должна задавать kind (man или completion). shell обязателен для автодополнения, имя файла которого не называет оболочку. section обязателен для man-страницы, имя файла которой не *.N. Пути по-прежнему ограничены содержимым.

ketch ставит ссылки на классифицированные файлы в пользовательский корень man и каталоги автодополнений, о которых сообщает ketch doctor. Места назначения записываются в state.json, как и бинарники, так что удаление и повторная расстановка ссылок используют то же доказательство владения.

asset

[asset]
include = ["*-apple-darwin.tar.gz"]   # должен совпасть хотя бы один
exclude = ["*-musl-*"]                # не должен совпасть ни один

[asset.target]
"macos-aarch64" = "*-aarch64-apple-darwin.tar.gz"

Приоритет, в порядке применения:

  1. exclude отбрасывает ассет окончательно, и ничто не может его вернуть.
  2. Подходящая запись asset.target для этого хоста прямо называет файл, поэтому она побеждает и include, и собственную оценку платформы.
  3. Иначе фильтрует include, а оставшееся оценивает платформа.

Ключи target — <os>-<arch> машины, на которой запущен ketch, например macos-aarch64, macos-x86_64, linux-x86_64, linux-aarch64, windows-x86_64 или windows-aarch64. Ключа universal нет: universal — свойство ассета, а не хоста.

Прибегайте к этому, только когда собственная оценка платформы выбирает неправильно. Сначала запустите ketch info <pkg> --assets — он перечисляет каждый ассет с полученной оценкой и причиной, и обычно этого достаточно, чтобы понять, что должен сказать манифест.

trust

Контрольная сумма доказывает, что загрузка — это тот файл, который указан в релизе. Подпись доказывает, кто его опубликовал. trust называет этого издателя, и ketch отклоняет релиз, не несущий его подписи.

[trust]
verifier = "sigstore"
issuer = "https://token.actions.githubusercontent.com"
repository = "BurntSushi/ripgrep"
КлючПрименяется кЧто делает
verifierвсем — обязательноsigstore, minisign или gpg.
modeвсемrequire (по умолчанию) отклоняет установку, если подпись не удаётся проверить. warn всё равно устанавливает, сообщает об этом и записывает пакет как проверенный только контрольной суммой.
signatureвсемИмя файла подписи рядом с ассетом; {file} обозначает имя подписанного файла. По умолчанию: {file}.sigstore.json, {file}.minisig, {file}.asc.
signedвсемГлоб, называющий список контрольных сумм (SHA256SUMS, *checksums.txt), который покрывает подпись вместо ассета. В списке должен быть ассет с хешем, который скачал ketch.
issuersigstore — обязательноOIDC-издатель сертификата подписи: https://token.actions.githubusercontent.com для GitHub Actions.
identitysigstoreТочная идентичность сертификата: URL workflow, например https://github.com/o/r/.github/workflows/release.yml@refs/tags/v1.2.3, или email.
repositorysigstoreowner/repo: любой workflow в этом репозитории GitHub. Обязательно identity, repository или оба — один лишь издатель пропускает любого, кому он выдаёт сертификаты.
public_keyminisign, gpg — обязательноminisign: строка ключа или весь minisign.pub. gpg: блок открытого ключа в armored-формате.
fingerprintgpg — обязательноПолный отпечаток основного ключа. Он должен быть в блоке ключа.

Что принимает каждый верификатор:

  • sigstore — бандл Sigstore, который пишут cosign sign-blob --bundle и аттестации артефактов GitHub. Он проверяется офлайн по публичному доверенному корню Sigstore (public-good trusted root), который ketch носит с собой: цепочка сертификата ведёт к CA Sigstore и называет закреплённую идентичность, подпись покрывает файл, а журнал прозрачности Rekor подписал запись. Голый .sig с сертификатом или .intoto.jsonl без записи журнала нельзя проверить офлайн, и они отклоняются.
  • minisign — подписи с предварительным хешированием, которые делает minisign -S. Устаревшие отклоняются.
  • gpg — отсоединённая подпись, armored или двоичная, сделанная ключом или одним из его подключей для подписи. Связка ключей не читается: ключ, импортированный для чего-то другого, не является разрешением этого издателя. Отозванные ключи, просроченные подписи и подписи, сделанные после истечения срока ключа, отклоняются.

Файл подписи и любой список контрольных сумм — такие же ассеты релиза, как устанавливаемый: релиз, который их не публикует, проваливает установку. То, что было проверено, записывается в state.json — верификатор, закреплённая идентичность, имя файла подписи и его SHA-256, а для sigstore — индекс в журнале Rekor, — и ketch info это показывает. Ничего из того, что файл подписи говорит о себе, не печатается. Пакет без trust отображается как checksum, если релиз опубликовал совпадающую контрольную сумму, и как first use, если не опубликовал никакой.

hooks

Команда, выполняемая в каждый из шести моментов жизни пакета. Каждая — одна строка для оболочки платформы: sh -c на macOS и Linux, cmd /C в Windows.

[hooks]
before_install = "echo about to install $KETCH_PACKAGE $KETCH_VERSION"
after_install = "./tool --setup"
before_update = "./tool --export > \"$KETCH_ROOT/tool-backup.json\""
after_update = "./tool --import \"$KETCH_ROOT/tool-backup.json\""
before_uninstall = "./tool --teardown"
after_uninstall = "rm -rf \"$HOME/.cache/tool\""
КлючКогда выполняется
before_installДо размещения первой установки пакета или переустановки уже установленной версии.
after_installПосле того как эта установка размещена, ссылки поставлены и всё записано.
before_updateДо того как другая версия заменит установленную, — ketch upgrade, ketch install другой версии или ketch rollback.
after_updateПосле того как новая версия размещена, ссылки поставлены и всё записано.
before_uninstallДо удаления ссылок пакета и его каталога в хранилище.
after_uninstallПосле того как они удалены и пакета больше нет в state.json.

Каждый хук получает одинаковое окружение поверх вашего:

ПеременнаяЗначение
KETCH_HOOKВыполняемый ключ: after_install и так далее.
KETCH_PACKAGEИмя пакета.
KETCH_VERSIONВерсия, которая устанавливается, до которой обновляют или которая удаляется.
KETCH_PREVIOUS_VERSIONЗаменяемая версия. Задаётся только для двух хуков обновления.
KETCH_PREFIXКаталог пакета в хранилище. В before_install его ещё нет, а в after_uninstall уже нет.
KETCH_BIN_DIRКуда ставятся ссылки на бинарники, по умолчанию ~/.ketch/bin.
KETCH_ROOTКорень ketch, по умолчанию ~/.ketch.

Рабочий каталог — KETCH_PREFIX, когда он существует, так что ./tool в хуке — это только что распакованный бинарник; иначе хук наследует каталог, из которого запущен ketch. Стандартный ввод закрыт. Вывод перехватывается: он идёт в лог, а с --verbose — ещё и в терминал.

Хук before_*, завершившийся с ненулевым кодом, останавливает операцию, а его stderr становится подробностями ошибки — ничего не размещается и ничего не удаляется. Упавший хук after_* — это предупреждение: установка или удаление уже произошли, и state.json это отражает, а упавший хук не делает это неправдой. Хук, который всё ещё работает через десять минут, останавливается вместе со всем, что он запустил, и считается упавшим.

ketch rollback — это обновление обратно до сохранённой версии, поэтому он выполняет два хука обновления, а KETCH_PREVIOUS_VERSION называет покидаемую версию. ketch prune удаляет старые копии пакета, который остаётся установленным в той же версии, и хуков не выполняет.

Хуки выполняются только из манифеста в вашем собственном каталоге манифестов, ~/.ketch/manifests/<name>.toml. Пакет реестра или встроенный манифест — чужой файл, и установка того, что он описывает, никогда не должна означать запуск чужих команд оболочки, — поэтому манифест из этих уровней, содержащий hooks, отклоняется при установке, до размещения чего-либо, с сообщением, какой файл куда скопировать. Копирование его в ваш каталог манифестов и есть явное согласие: он становится вашим и с этого момента побеждает копию из реестра.

Что проверяет ketch

Serde проверяет форму. Вот значения, о которых он судить не может:

ПравилоПочему
name должно быть пригодно дословно как один компонент путиОно становится каталогом в хранилище. Имя, которое пришлось бы очищать, установилось бы не туда, куда указывает.
каждое bin.name должно быть пригодно дословно как один компонент путиОно становится симлинком в ~/.ketch/bin.
каждый bin.path должен оставаться внутри содержимогоНикаких абсолютных путей и никаких ...
каждая запись extra_paths должна оставаться внутри содержимогоТо же самое.
каждая запись extra_paths должна быть man-страницей или автодополнениемКлассифицируется по kind или по правилам путей ниже. Неоднозначные записи отклоняются.
каждой записи bin нужен name или pathЗапись без обоих ничего не говорит.
алиасы provides должны быть непустыми и без пробельных символовАлиас, который никто не может набрать, — не алиас.
strip_prefix должен быть не больше 8Каждый уровень — листинг каталога, и ни один реальный архив не вкладывает обёртки так глубоко.
каждая запись hooks должна что-то содержатьПустая всё равно запустила бы оболочку и сообщила бы об успехе.
манифест с hooks должен быть из вашего каталога манифестов — проверяется при установке, а не при загрузкеМанифест из реестра или встроенный — чужой файл, и его команды — чужой код.
trust.signature и trust.signed должны быть пригодными именами файлов, а signature знает только {file} и должен называть другой файлОни называют файлы, скачиваемые рядом с ассетом.
ключи trust должны относиться к выбранному verifierfingerprint в политике minisign выглядел бы как проверка, но ею не был бы.
sigstore нужен issuer с https и identity или repositoryОдин лишь издатель пропускает любого, кому он выдаёт сертификаты.
public_key для minisign должен разбиратьсяКлюч, который нельзя прочитать, нельзя закрепить.
public_key для gpg должен разбираться, проходить проверку собственных самоподписей, не быть отозванным и иметь закреплённый fingerprintОтпечаток и есть закрепление; сам по себе блок ключа мог бы принадлежать кому угодно.

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

Как опробовать манифест

Положите его в ~/.ketch/manifests/<name>.toml и установите:

ketch install <name> --verbose

--verbose сообщает, с какого уровня пришёл манифест, какой ассет был выбран и почему и на что поставлены ссылки. Чтобы предложить его всем, отправьте тот же файл в реестр как <name>/ketch.toml — см. REGISTRY.md.