/
Поиск
RU

Пакеты (kola.pack)

Один файл .kl — это один модуль. Когда программа перерастает один файл или когда кодом хочется поделиться с другими программами, файлы складывают в пакет — каталог с манифестом kola.pack. Манифест даёт пакету имя и версию, перечисляет зависимости и в нескольких строках сообщает, насколько далеко во внешний мир может дотянуться код пакета. kola audit сверяет это утверждение с кодом. Ниже речь пойдёт о структуре пакета, манифесте, аудите, зависимостях и публикации.

Что такое пакет

Пакет — это каталог с файлами .kl и манифестом kola.pack в формате TOML, простом формате конфигурации из [разделов] и строк ключ = значение. Имя из [pack] name становится корнем импорта для файлов внутри пакета — точно так же, как std служит корнем импорта стандартной библиотеки. В пакете framing файл src/frame.kl — это модуль framing::frame.

Файл, над которым нет никакого kola.pack, может импортировать только из std. Чтобы использовать код из другого каталога, этот код должен быть пакетом, а ваш файл должен лежать в пакете, манифест которого объявляет его зависимостью.

Модули внутри пакета работают так же, как вне его: pub, use pack::module;, блоки impl — всё как в главе Модули, стандартная библиотека и fn-ссылки. Пакет — это граница вокруг модулей, а не новый вид модуля.

Структура

Пакет плоский: каталог и есть пакет. Манифест лежит в его корне, исходники — в каталоге, который назван в [pack] src, а рядом — необязательные каталоги examples/ и docs/ и обычные README.md, CHANGELOG.md, LICENSE и NOTICE. kola pack build упаковывает в архив ровно это дерево.

framing/
  kola.pack        имя, версия, бюджет, зависимости
  src/
    frame.kl       use framing::frame;
  examples/
    main.kl        программа, собранная поверх пакета
  docs/            попадает в архив
  README.md  CHANGELOG.md  LICENSE  NOTICE

Команда kola pack new <name> [--license <spdx-id>] [--bin] создаёт такой скелет: манифест с комментариями, один модуль, один тест и один пример.

Корень исходников определяется в фиксированном порядке. Явный [pack] src важнее всего. Если его нет, берётся каталог файла из [pack] entry. Если нет ни того, ни другого, исходники лежат в корне пакета. kola никогда не ищет на диске «подходящий» каталог, так что пустой src/ ничего не меняет. Если src и entry указывают на разные корни, манифест отвергается. Корень исходников влияет только на разрешение use: все пути, записанные в самом kola.pack (например, [embed] files), отсчитываются от корня пакета.

Библиотека обычно объявляет src и не объявляет entry. entry называет файл, в котором живёт main, а у многих библиотек нет одного главного модуля, и записать туда нечего. Пакет-программа (kola pack new --bin) указывает свою точку входа, и kola audit проверяет, что такой файл существует.

Старая вложенная структура <name>/<name>/kola.pack по-прежнему разрешается. Каталог, у которого есть собственный kola.pack и при этом есть вложенный пакет, отвергается: инструмент не может понять, пакет это или коллекция пакетов.

Файлы в examples/ — цели сборки пакета, а не отдельные пакеты. Они компилируются с пакетом в области видимости и манифеста не требуют. kola check . и kola pack build . компилируют их, поэтому пример, переставший собираться, ломает сборку; kola run examples/hello.kl запускает пример.

Манифест

Полный манифест небольшой библиотеки:

[pack]
name        = "widget"
version     = "0.1.0"
src         = "src"
description = "one line for the registry card"
license     = "Apache-2.0"

[caps]
allowed = ["Env"]

[caps.tests]
allowed = ["Rng"]

[caps.examples]
allowed = ["Sys", "log"]

Раздел [pack] принимает ровно эти ключи: name, version, src, entry, description, license, source и c_abi. source — справочная ссылка на место, где лежит код (скажем, URL git-репозитория). kola её печатает, но никогда по ней не ходит, поэтому манифест из скачанного архива не может перенаправить следующую загрузку. c_abi перечисляет модули, которые вызываются из C; об этом — в главе Встраивание kola в C-программу.

Неизвестный ключ — ошибка, а не то, что можно молча пропустить. Строка манифеста, которую никто не проверяет, — это утверждение, которое никто не подтверждает, а манифест существует именно для утверждений, которым рецензент может доверять. Сообщение компилятора перечисляет известные ключи и подсказывает ближайший:

error[pack manifest]: malformed manifest: `[pack] vresion` is not a key this compiler knows — ... Known keys: c_abi, description, entry, license, name, source, src, version. Did you mean `version`?

Ключа для версии языка нет. Пакет собирается по той карте, которую знает компилятор (её печатает kola --version), а строка card = ... отвергается с предложением её удалить.

Остальные разделы:

Раздел

Что в нём

[caps]

бюджет капабилити для кода библиотеки

[caps.tests], [caps.examples]

дополнительный бюджет для элементов test и для examples/*.kl

[deps]

зависимости с точными версиями

[deps.examples], [deps.tests]

зависимости, доступные только примерам или только тестам

[embed]

статические файлы, вкомпилированные в бинарник

[leaks]

необязательный, принудительно проверяемый бюджет утечек регионов

[hw]

файлы C или ассемблера для конкретных архитектур под клаузы hw (SPEC-CARD §8)

[dev]

reload — граница для kola run --reload (kola help reload)

Манифест, который не разбирается однозначно (неизвестный ключ, повторённый раздел, устаревшее написание), даёт error[pack manifest], и его отвергает любая команда, включая kola check и kola test. Чекер останавливается, не читая ваш код: бюджет, который нельзя прочесть однозначно, нельзя и проверить.

Бюджет капабилити

Сердце манифеста — [caps] allowed, бюджет капабилити. В kola функция может обратиться к внешнему миру (часам, файлам, сети) только через капабилити — значение, которое и есть разрешение это делать. main получает весь набор, Sys, и раздаёт его части дальше. Бюджет перечисляет все типы капабилити, которые код пакета может получить из Sys. Пакет, у которого в списке есть Clock, может узнавать время; пакет с Dial может открывать исходящие соединения; пакет с пустым списком не может ни того, ни другого, и никакой код внутри этого не изменит.

[pack]
name = "kolanative"
version = "0.1.0"

[caps]
allowed = []

Это настоящий манифест: генератор кода, который собирает машинные инструкции в буфер. С allowed = [] он не может тронуть ни файл, ни сокет, ни часы, и рецензенту не нужно читать функции кодировщика, чтобы в этом убедиться. Парсеры, реализации протоколов и структуры данных должны выглядеть так же. Если библиотеке нужно время, обычно достаточно принять параметр now: u64 вместо Clock; если нужна сеть — работать с соединением, которое открыл вызывающий.

Список принимает имена капабилити внешнего мира из SPEC-CARD §7: Args, Clock, Cpu, Dev, Dial, Env, Fs, Inherited, Listen, Mem, Mmio, Netlink, Ping, Proc, RawIp, Rng, Sim, Stdin, Stdout, Sys, Tty, Window и ещё одно имя в нижнем регистре, log. Несколько пояснений:

  • Fs покрывает всё семейство файловой системы. File или Dir отдельно не указывают: без Fs их не получить.

  • Stdout и Stdin — разные строки: запись вывода программы и чтение её ввода — разные вопросы для аудита.

  • Ping отделён от Dial: умение прочесать сеть ICMP-запросами не следует из умения открыть соединение.

  • Dev — выданное символьное устройство, собеседник со своим протоколом. Mmio относится только к окнам регистров, чтобы в манифесте это слово означало ровно это.

log — эффект, а не капабилити: это ряд !{log} (SPEC-CARD §6). Никакая капабилити его не даёт, поэтому он указывается отдельно. Приёмник журнала доступен только на запись, но это всё равно канал наружу из программы, и вопрос «что этот пакет пишет в журнал» заслуживает собственной строки. kola audit печатает его как log (effect).

Sys в списке разрешает Sys и больше ничего. Если бы он покрывал всё, то allowed = ["Sys"] (естественная строка для любого пакета с main(sys: Sys)) ничего не говорила бы о том, что делает пакет, а все остальные записи стали бы лишними. Поэтому пакет, который обращается к sys.clock, обязан указать и Clock, а аудит сообщит, если этого нет.

Что измеряет бюджет

Бюджет ограничивает то, что пакет может взять из корневого набора, но не то, что пакету передают. Если функция получает Conn параметром, значит, кто-то другой потратил Dial или Listen, чтобы создать это соединение, и полномочие принадлежит вызывающему. Библиотека HTTP-сервера, которая обслуживает запросы по сокетам, принятым вызывающим, совершенно правильно не объявляет Listen вовсе.

То же верно для любого значения, созданного в другом месте: линейных дескрипторов (Conn, File, Device, UdpSock, ...), выданных корней (Dir, DirRo), маркеров аппаратных окон (MmioWindow, RamWindow, RamWindowRo, IoWindow), Cpu, а также fn-ссылок, в ряде эффектов которых есть io, block или hw. Замыканий в kola нет, поэтому fn-ссылка — способ передать библиотеке «шов» во внешний мир (q: fn(&[u8]) -> u64 !{io, block}); библиотека, которая её вызывает, сама ничего не держит. Пакет, который только работает с тем, что ему передали, проходит аудит с allowed = [], и это правильный манифест для него. Объявив там Dial, вы заставили бы бюджет утверждать больше, чем пакет умеет.

Значит, на вопрос «до чего этот пакет может дотронуться» бюджет не отвечает. На него аудит отвечает отдельно, строкой reaches: (см. ниже).

Выдачи по имени

Некоторые полномочия выдаются по имени: каталог, устройство, запускаемый бинарник, аппаратное окно. Оператор привязывает имя при запуске программы (--fs-root db=/srv/db, --proc-bin ffmpeg=/usr/bin/ffmpeg), а программа запрашивает его по имени (sys.fs.root(b"db")). Манифест перечисляет имена, а не только тип капабилити:

[caps]
allowed      = ["Fs", "Mmio", "Dev", "Proc", "Dial"]
fs_roots     = ["db", "zones:ro"]       # sys.fs.root(b"db") / sys.fs.root_ro(b"zones")
mmio_windows = ["uart0"]                # sys.mmio.window(b"uart0")
ram_windows  = ["ring0"]                # sys.mmio.ram(b"ring0")
io_windows   = ["com1"]                 # sys.mmio.io_window(b"com1"), порты ввода-вывода x86
devices      = ["widget"]               # sys.dev.open(b"widget")
proc_bins    = ["ffmpeg"]               # sys.proc.spawn(b"ffmpeg", ..)
dial_hosts   = ["api.example.com:443"]  # dial.to_host(b"api.example.com", 443)

Поэтому аудит сообщает «хочет db и zones», а не «хочет файловую систему». Нарушением считается и запрос имени, которого нет в списке, и запрос на чтение-запись (root) к корню, помеченному в манифесте как :ro. Полный набор списков имён: fs_roots, mmio_windows, ram_windows, io_windows, devices, proc_bins, dial_hosts, extern_syms, reserves, netlink и perturbs.

Бюджеты тестов и примеров

Тестам и примерам обычно нужно больше, чем коду библиотеки. Тест может поднимать сервер на loopback-интерфейсе, а main(sys: Sys) в примере держит весь корневой набор. Если записать это в [caps] allowed, каждый рецензент решит, что так может поступать сама библиотека.

Поэтому у каждой цели свой раздел:

[caps]
allowed = []

[caps.tests]
allowed = ["Dial", "Listen"]
fs_roots = ["fixtures"]

[caps.examples]
allowed = ["Sys", "log"]

Правил три. Ключи в таком разделе называются так же, как в [caps]: allowed и все остальные ключи из списка выше. Бюджет раздела добавляется к [caps], так что капабилити, уже разрешённую там, повторять не нужно. И разделение идёт по достижимости, а не по расположению в файле: закрытая вспомогательная функция, которую вызывают только элементы test, считается тестовым кодом, а закрытая функция, которую не вызывает никто, считается кодом библиотеки.

Старое написание — ключ tests = [...] внутри [caps] — отвергается с сообщением, которое называет нужный раздел.

Запуск kola audit

Возьмём манифест widget из примера выше, но с allowed = ["Env", "Rng"], и такой модуль библиотеки:

use std::text::utf8;
use std::net;

// Читает одну переменную окружения: тратит Env, разрешённую манифестом.
pub fn home_dir(env: &Env) -> Option[&heap str] !{io, alloc(heap)} {
    match env.get(&b"HOME") {
        Some(bytes) => utf8::to_str(bytes),
        None => None,
    }
}

// Пишет в соединение, которое уже открыл вызывающий.
pub fn serve_one(conn: &mut Conn, out: &[u8]) -> Result[(), (NetErr, OsErr)] !{io, block} {
    net::send_all(conn, out)
}

Аудит печатает обычный текст, потому что читает его человек:

$ kola audit widget
audit: caps within budget — pack `widget` (used=["Env"] ⊆ allowed=["Env", "Rng"])
audit: examples — used=["Env", "Sys"] ⊆ allowed=["Sys", "log (effect)"] (additive to `[caps] allowed`)
warning[pack caps unused]: `widget` declares `Rng` in `[caps] allowed` but nothing in the pack needs it — not its sources, not a `test` item, not an example target. ...
audit: reaches: Conn — already-minted authority value(s) on this pack's pub fn signatures ...
audit: leaks — ...
audit ok: 0 violation(s) — every budget checked in this run is within its declared limits

Здесь важны четыре вещи. Первая строка — вердикт для библиотеки: она использует Env, и это разрешено. Вторая — то же для примеров. Предупреждение говорит, что Rng разрешён, но не используется; это предупреждение, а не ошибка, потому что пакет может резервировать бюджет намеренно, однако бюджет шире кода вводит рецензента в заблуждение. Строка reaches: называет Conn — полномочие, которое проходит через публичные сигнатуры пакета, но создано не в нём. Она ничего не запрещает; она отвечает на вопрос о «радиусе поражения», на который бюджет ответить не может.

Строки leaks — перепись утечек регионов (мест, где память может накапливаться в регионе, который освободится ещё не скоро) для исходников самого пакета, для его зависимостей и для примеров; подробно о них — в главе Утечки регионов. Последняя строка — итоговый вердикт, и audit ok печатает только успешный прогон. Неудачный заканчивается строкой audit: FAILED (n violation(s)) и кодом выхода 1.

Для скриптов есть --json (или --format json): он добавляет по одному JSON-объекту на пакет с полями caps_used и caps_budget, такими же парами для каждого списка имён и каждого раздела (fs_roots_used, caps_used_tests, ...), полем effects, полем authority_reached для набора reaches: и объектом leaks. Текст уходит в stderr, так что stdout под --json содержит чистый JSON. Если вы разбираете вывод аудита в CI, явно указывайте --json.

kola audit, запущенный на каталоге с несколькими пакетами, проверяет каждый пакет сам по себе и печатает один общий вердикт. kola pack ci создаёт CI-процесс (скрипт, который git-сервер запускает при каждом push), который прогоняет аудит за вас. Бюджет, который никто не проверяет, — всего лишь комментарий.

Ошибки в обе стороны

Аудит сверяет код с манифестом, а манифест — с набором настоящих капабилити. Этот пакет совершает сразу три ошибки:

[pack]
name    = "badcaps"
version = "0.1.0"
src     = "src"

[caps]
allowed = ["Sys", "Io"]
use std::log;

pub fn tick(sys: &Sys) !{io, log, block} {
    log::info("tick");
    sys.clock.sleep(1u64);
}
$ kola audit badcaps
error[pack budget]: `Io` in [caps] allowed is not a capability this compiler knows — ... Known names: Args, Clock, Cpu, Dev, Dial, Env, Fs, Inherited, Listen, Mem, Mmio, Netlink, Ping, Proc, RawIp, Rng, Sim, Stdin, Stdout, Sys, Tty, Window, log. `Io` was RETIRED as a capability ... add whichever of `Stdout`/`Stdin`/`log` the pack actually reaches
error[pack budget]: capability `Clock` used on the production surface but not in the pack budget [caps] allowed = ["Sys", "Io"] — note `Sys` no longer bundles sub-capabilities ...
error[pack budget]: pack performs a `log` effect (!{log}) but its budget does not grant it — add `log` to [caps] allowed = [...]. ...
audit: 3 budget violation(s) in pack `badcaps`
audit: FAILED (3 violation(s)) — see the `error[...]` line(s) above; `kola audit` exits 1

Когда-то Io была капабилити, теперь её нет. Запись, которая ничего не называет, — ошибка, а не предупреждение: рецензенту она неотличима от записи, за которой стоит настоящее полномочие. Для частых неверных имён сообщения конкретны: устаревшему имени подсказывают, что его заменило; дескриптору вроде Conn или File предлагают объявить капабилити, которая его создаёт, «или ничего, если дескриптор приходит только параметром»; производной капабилити вроде Dir или MmioWindow называют исходную капабилити и соответствующий список имён. Вторая ошибка показывает, что Sys не покрывает sys.clock, третья — что запись в журнал нужно объявлять.

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

kola check выполняет тот же анализ бюджета, но выдаёт нарушения как предупреждения и продолжает компиляцию, чтобы вы видели и ошибки типов. kola run и нативные исполнители пакет с превышенным бюджетом отвергают. Поэтому зелёный check ничего не говорит об audit: запускайте оба.

Зависимости

Пакет перечисляет импортируемые пакеты в [deps]:

# app/kola.pack
[pack]
name  = "app"
version = "0.1.0"
src   = "src"
entry = "src/main.kl"

[caps]
allowed = ["log"]

[deps]
mid = { version = "0.1.0", caps = [] }
// app/src/main.kl
use mid::calc;
use std::log;

fn main() -> u8 !{log} {
    let a = calc::answer();
    log::info(f"answer={a}");
    0
}

test chain_resolves {
    assert calc::answer() == 42;
}

(Этот файл проходит проверку только внутри своего пакета, рядом с пакетом mid, который он импортирует.)

version сравнивается с собственным [pack] version зависимости как строка, на точное совпадение. Многие менеджеры пакетов принимают диапазоны вроде «любая 1.x новее 1.2» и запускают решатель, который подбирает версии, устраивающие все пакеты дерева; в kola нет ни диапазонов, ни решателя. Если версии расходятся, сборка останавливается, не прочитав ни одного исходника:

error[card §1]: /.../app/kola.pack: `[deps] mid = { version = "0.2.0", .. }` does not match the pack's own `[pack] version = "0.1.0"` (/.../mid/kola.pack) — kola v1 uses literal version equality, not semver ranges
  = help: update `[deps] mid`'s version to "0.1.0" in /.../app/kola.pack, or bump the depended-on pack's own version to match

Отвергаются также пакет, который зависит от самого себя, повторённый ключ и имя, объявленное в двух разделах зависимостей.

Список caps в записи повторяет собственный [caps] allowed зависимости. Аудит проверяет, что ваш список не уже объявленного зависимостью, поэтому зависимость, которой вдруг понадобилась новая капабилити, ломает вашу сборку, пока вы не разрешите её в своём манифесте. Запись зависимости может содержать и списки имён (fs_roots, proc_bins и так далее); о неизвестном компилятору ключе сообщается.

Транзитивные зависимости

Зависимости разрешаются транзитивно. Если app зависит от mid, а mid — от leaf, то app объявляет только mid. Он не импортирует leaf, не вызывает его, и знать его имя ему незачем.

Работает это благодаря правилу о владельце. use root::... в модуле допустим, если root — это std, пакет, которому принадлежит модуль, или одна из [deps] этого пакета-владельца. Значит, mid может импортировать leaf, потому что так сказано в манифесте mid. Если mid забыл его объявить, ошибка называет mid — единственный пакет, который может это исправить:

error[card §1]: `use leaf::core` in `mid::calc` (a module of pack `mid`): `leaf` is not `std`, `mid`'s own modules, or one of `mid`'s declared `[deps]` (none declared) — a dependency pack must declare its OWN dependencies; add `leaf = { version = "..." }` under `[deps]` in `mid`'s kola.pack and it resolves transitively from there

Версии проверяются на каждом шаге. Цикл (a зависит от b, а b от a) — ошибка, в которой напечатан сам цикл. «Ромб», когда две ваши зависимости зависят от одного и того же третьего пакета, разрешается в один экземпляр этого пакета.

Зависимости примеров и тестов

Иногда примеру нужен пакет, который не нужен библиотеке. Например, клиенту из примера нужен сервер, с которым можно разговаривать. Если объявить этот сервер в [deps], каждый пользователь библиотеки унаследует зависимость, которой библиотека не пользуется, поэтому место ей в [deps.examples]. Записи там устроены так же и подчиняются тем же правилам, что и [deps], но разрешаются только из файлов в examples/, а из src/ отвергаются.

[deps.tests] делает то же для тестов. Поскольку элемент test живёт в файле библиотеки и делит с ним строки use, границу проверяют по графу вызовов: каждая функция, которая обращается к тестовой зависимости, должна быть элементом test или функцией, до которой доходят только тесты. pub fn, main или что угодно, достижимое из поставляемого кода, — нарушение:

error[pack budget]: `leaf` is declared under `[deps.tests]`, and `seeded` (tk/src/lib.kl) calls `core::base` while a shipping path reaches it — a test-only dependency may be called only from `test` items and from fns EVERY path to which starts in one ... Move the call into a `test`-only helper, or move `leaf` to `[deps]` if the library really depends on it — that is a claim every consumer inherits

Ни тот, ни другой раздел не наследуется: пользователь вашего пакета никогда не видит ваших тестовых зависимостей и зависимостей примеров.

Где ищется зависимость

kola ищет зависимость в четырёх местах в таком порядке: файл рабочего пространства kola.work, соседние каталоги, локальный кэш ~/.kola/packs и, наконец, реестр — сервер с опубликованными пакетами — по сети.

Файл kola.work, который ищется вверх по дереву от пакета, перечисляет рабочие копии в [workspace] use. Так можно править зависимость и её пользователя одновременно. kola проверяет этот файл при каждом check, build и audit, даже у пакета без зависимостей: запись, которая не является каталогом, не содержит читаемого kola.pack или объявляет имя, уже объявленное другой записью, — ошибка.

error[card §1]: /.../kola.work: `[workspace] use` entry `./nowhere` does not resolve to a directory (/.../nowhere): No such file or directory (os error 2) — delete the entry, or fix the path if the checkout moved
  = help: fix the kola.work workspace file, or delete the offending `use` entry

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

Поиск по соседям смотрит в каталоги рядом с вашим пакетом и ищет kola.pack, у которого [pack] name совпадает с именем зависимости. Черновой файл в /tmp вообще не может импортировать пакет; положите его внутрь пакета.

Дерево аудита

Атака на цепочку поставок приходит через код, от которого вы зависите, а не через код, который вы написали. Основной риск сидит внутри зависимостей, поэтому kola audit обходит всё разрешённое дерево и печатает по блоку на пакет:

$ kola audit app
audit: caps within budget — pack `app` (used=[] ⊆ allowed=["log (effect)"])
audit: dependency tree (1 declared dep(s) of `app`):
  dep `mid` (/.../mid):
    audit: caps within budget — pack `mid` (used=[] ⊆ allowed=[])
    dep `leaf` (/.../leaf):
      audit: caps within budget — pack `leaf` (used=[] ⊆ allowed=[])
audit ok: 0 violation(s) — every budget checked in this run is within its declared limits

Проверки складываются по нескольким правилам:

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

  2. На каждом ребре список caps, который импортирующий пакет записал для зависимости, должен покрывать собственный [caps] allowed этой зависимости. Нарушение называет непосредственного импортёра — пакет, в манифесте которого принимается это решение, — а не корень, отстоящий на несколько шагов.

  3. Бюджеты не суммируются. Разрешение одной зависимости никогда не попадает в общий котёл, из которого могла бы черпать соседняя, и проверки «сумма всех капабилити в дереве» нет. Раз внешних полномочий по умолчанию не существует, капабилити, которой зависимость пользуется от вашего имени, может дойти до вашего кода только через значение или эффект в ваших же сигнатурах, а их уже проверяет пункт 1.

  4. Тестовый бюджет зависимости ([caps.tests]) сверяется только с её собственным манифестом. Её тесты вы не запускаете, значит, и разрешать то, что им нужно, не должны.

Если leaf в своём манифесте получил доступ к часам, а mid этого не повторил, аудит падает на этом ребре:

error[pack deps budget]: dependency `leaf` (pack `leaf`) declares `[caps] allowed` includes `Clock`, but `mid`'s `[deps] leaf` sub-declaration does not list it — a dependency update that starts wanting a new capability is a build error until the importing pack's manifest explicitly grants it

Аудит также предупреждает о записи в [deps], которую не импортирует ни один модуль, и о разрешении в caps, которого зависимость больше не просит. Код выхода ненулевой, если хоть один пакет в дереве нарушил свой бюджет или хоть одно ребро не прошло проверку покрытия.

Встроенные файлы

Пакет может вкомпилировать статические файлы (HTML, CSS, картинки) прямо в бинарник, вместо того чтобы читать их с диска во время работы:

[embed]
files = ["web/index.html", "web/app.css"]
use std::embed;

pub fn index_page() -> &heap [u8] !{} {
    embed::bytes("web/index.html")
}

embed::bytes принимает строковый литерал, который сверяется со списком при проверке (файл не из списка или отсутствующий файл — ошибка компиляции). Капабилити не нужна, эффекта нет. Аудит печатает каждый встроенный файл с размером и SHA-256 и предупреждает о файле из списка, который не читает ни один вызов embed::bytes: он всё равно попадёт в архив. (Фрагмент проходит проверку только в пакете, где этот файл перечислен.)

Бюджет [leaks]

[leaks] необязателен. Без него строки аудита об утечках носят справочный характер. С ним аудит принудительно проверяет в исходниках самого пакета две вещи: потенциальные места утечек должны быть признаны по ключу (acknowledged = [...]), а not_analyzed_pct_cap = N ограничивает процент нетестовых элементов пакета, оказавшихся вне проверяемого IR (той части кода, по которой может пройти анализ утечек). Зависимости и примеры отражаются в отчёте, но в бюджет не входят: признать место, которое написали не вы, нельзя. Раздел [leaks] в пакете, где ни одна проанализированная функция не выделяет память, отвергается — такой бюджет никогда не сработает. Подробности — в главе Утечки регионов.

Архивы, файлы блокировки и локальный реестр

Манифест и вердикт стоят ровно столько, сколько байты, которые они описывают. Архив .kpack хранит дерево пакета в детерминированном виде: одно и то же дерево всегда даёт одни и те же байты. Хеш SHA-256 — это 32-байтовый отпечаток содержимого: изменение любого байта меняет его, и подобрать другое содержимое с тем же хешем практически невозможно. Поэтому SHA-256 архива служит идентичностью этой версии пакета.

$ kola pack build widget -o widget.kpack
{"name":"widget","version":"0.1.0","description":"a demo pack","license":"Apache-2.0","files":9,"bytes":14053,"sha256":"f2a8c469...","out":"widget.kpack","source":""}

$ kola pack verify widget.kpack
{"ok":true,"files":9,"sha256":"f2a8c469..."}

Прежде чем что-то записать, kola pack build выполняет те же проверки, что и публикация: сверяет манифест с описываемыми файлами (корень исходников существует, entry существует, поставляемый LICENSE назван), требует чистого kola check исходников и примеров, проводит аудит бюджета и проверку самодостаточности (ни комментарий, ни строка манифеста не может ссылаться на документ, которого нет в архиве, и никакой ../ не может выйти за пределы пакета). kola pack check <dir> выполняет те же проверки, не записывая архив, для одного пакета или каталога пакетов, и печатает по строке ok или FAIL на пакет.

Проверка лицензии нужна потому, что реестр индексирует пакеты по [pack] license, а не разбирает текст лицензии. В этом ключе стоит идентификатор SPDX — стандартное короткое имя лицензии, например MIT или Apache-2.0. Пакет, который поставляет файл LICENSE, но не называет лицензию, выглядел бы нелицензированным, хотя лицензию несёт:

error[pack license]: lic/kola.pack: this pack ships lic/LICENSE — `kola pack build` puts it in the archive — but `[pack] license` is absent, so the manifest does not say which licence that text IS. ... Add `license = "<SPDX id>"` under `[pack]` (e.g. `license = "Apache-2.0"`), or delete LICENSE if this pack really carries no licence (design-packages.md §4.1). The SPDX id is NOT compared against the file's text.

Обратное допустимо: манифест может назвать лицензию, даже если файла рядом нет.

kola pack lock <dir> записывает kola.lock — файл блокировки, где для каждой зависимости зафиксированы точная версия и хеш содержимого, чтобы последующие сборки могли убедиться, что используют те же байты. С этого момента kola audit падает, как только содержимое зависимости меняется:

error[pack lock]: dependency `mid` resolved to content hash 03d0393d... but kola.lock pins 84cbd16b... — drift (re-run `kola pack lock` if the bump is intended)

Хеш покрывает содержимое, а не релизы, поэтому любая правка зависимости (даже комментария) делает блокировку недействительной; её пересоздают, а не правят руками. Пакет с [deps] и без файла блокировки получает warning[pack lock] при каждом аудите. Стоит помнить о двух ограничениях. Блокировку проверяет только kola audit; check, test и build успешно проходят на изменившейся зависимости. И аудит проверяет только файл блокировки корневого пакета: собственная блокировка зависимости проверяется, когда аудит запускают для неё.

Локальный реестр — это каталог, по умолчанию ~/.kola/registry (его можно переопределить через $KOLA_REGISTRY или назвать дополнительные реестры в ~/.kola/settings.toml, по таблице [registry.<name>] на каждый):

kola pack push widget local          # опубликовать после тех же проверок, что и build
kola pack fetch widget@0.1.0 local   # распаковать в ~/.kola/packs с проверкой хеша

Опубликованная версия неизменяема. Повторная публикация тех же байтов ничего не делает; публикация других байтов под той же версией отвергается. kola pack sync <dir> за один проход загружает в кэш всё дерево зависимостей, а kola pack outdated <dir> показывает прямые зависимости, у которых есть более новые версии.

Удалённые реестры и подписи

Основной реестр pack.kola.works принимает подписанные публикации по HTTPS. Подпись работает так: закрытый ключ издателя создаёт подпись над архивом, и любой, у кого есть парный открытый ключ, может проверить, что именно этот издатель выпустил именно эти байты. Вход выполняется командой kola pack login, ключ подписи ed25519 создаётся командой kola pack key generate (он хранится локально или, с --backend conclave, в хранилище Conclave Transit, откуда закрытый ключ никогда не выходит), а kola pack push <dir> [registry] [--key <name>] подписывает архив при отправке.

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

Запускать fetch вручную приходится редко. Если check, run, test или build нужна зависимость, которой нет ни в рабочем пространстве, ни среди соседей, ни в кэше, и у неё есть закрепление (запись в kola.lock или точная версия в [deps], для которой реестр может назвать хеш), kola сама загружает её в кэш. Скачанный архив с неверным хешем — ошибка, в которой названы оба хеша. Зависимость без закрепления сеть не трогает: ошибка предлагает запустить kola pack lock. --offline (или KOLA_OFFLINE=1) запрещает любую загрузку зависимостей — для герметичных сборок. kola security check <dir> сверяет разрешённое дерево с опубликованными в реестре уведомлениями об уязвимостях.

Чего у пакетов нет

Некоторых вещей нет намеренно:

  • Бинарных зависимостей. Зависимости между пакетами kola всегда поставляются в исходниках. Контракты, эффекты и линейность проверяются по исходному коду, а бинарному пакету можно было бы только верить, но не перепроверить его. Обойти это тоже нечем: в языке нет ни unsafe, ни extern.

  • Диапазонов версий. Версии совпадают точно на каждом шаге, решать нечего.

  • Кода, выполняемого при установке. До компиляции пакет — инертные данные. Ни скриптов сборки, ни хуков установки нет.

Все правила на этой странице при сомнении запрещают. Отсутствующий раздел [caps] — пустой бюджет, а не безграничный. Манифест, который парсер не может прочитать однозначно, — ошибка, а не повод угадать. А проверка опирается на типы и ряды эффектов, доказанные чекером, а не на текстовый поиск, который строка в другом форматировании могла бы обойти.

Обоснование правил дерева аудита — в docs/design-packages.md §3 в репозитории; справочник по командной строке — kola help kpack.