Манифесты
Манифест — это конфигурация одного пакета: откуда он берётся, какой ассет релиза брать и что выставлять наружу после распаковки.
Он нужен редко. ketch выводит манифест из одного лишь owner/repo, и для большинства
проектов этого достаточно. Пишите манифест, когда вывод ошибается: ассет с
необычным именем, бинарник, на который нужно поставить ссылку под другим именем,
бандл .app, короткий алиас, который стоит запомнить.
Одна и та же схема используется в трёх местах:
| Где | Файл | Для кого |
|---|---|---|
| Ваша машина | ~/.ketch/manifests/<name>.toml | только для вас |
| Реестр пакетов | <package>/ketch.toml | для всех — см. REGISTRY.md |
| Встроено в ketch | crates/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 для людей. |
kind | auto | binary | app | auto | Чем является содержимое. См. ниже. |
provides | список строк | пусто | Другие имена, на которые откликается этот пакет. ketch install rg работает, потому что ripgrep предоставляет rg. |
prerelease | bool | false | Учитывать пре-релизы при разрешении 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 — получают ссылки как обычно.
Выбор такой:
- тот, который назван в
--bin <name>уketch installилиketch upgrade, без учёта регистра и.exe; имя, не совпадающее ни с одним бинарником релиза, — ошибка; - иначе тот, чьё имя совпадает с именем пакета, —
rtok, но никогда неrtok-hook, — без учёта регистра и.exe; - иначе тот, который был выбран для этого пакета раньше:
state.jsonпомнит его между обновлениями, переустановками,ketch linkиketch rollback, аketch syncтакже читает его из ключаbinвketch.lock(сначала состояние, затем lock-файл); - иначе тот, который вы выберете из нумерованного списка, когда ketch работает в
терминале и
--yesне передан; - иначе ничего: установка завершается ошибкой со списком кандидатов и записью
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"
Приоритет, в порядке применения:
excludeотбрасывает ассет окончательно, и ничто не может его вернуть.- Подходящая запись
asset.targetдля этого хоста прямо называет файл, поэтому она побеждает иinclude, и собственную оценку платформы. - Иначе фильтрует
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. |
issuer | sigstore — обязательно | OIDC-издатель сертификата подписи: https://token.actions.githubusercontent.com для GitHub Actions. |
identity | sigstore | Точная идентичность сертификата: URL workflow, например https://github.com/o/r/.github/workflows/release.yml@refs/tags/v1.2.3, или email. |
repository | sigstore | owner/repo: любой workflow в этом репозитории GitHub. Обязательно identity, repository или оба — один лишь издатель пропускает любого, кому он выдаёт сертификаты. |
public_key | minisign, gpg — обязательно | minisign: строка ключа или весь minisign.pub. gpg: блок открытого ключа в armored-формате. |
fingerprint | gpg — обязательно | Полный отпечаток основного ключа. Он должен быть в блоке ключа. |
Что принимает каждый верификатор:
- 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 должны относиться к выбранному verifier | fingerprint в политике 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.