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

Подписи на изображении — на английском.
Пакет из трёх файлов не собирался. Манифест объявлял зависимость по URL без хеша, поэтому загрузка не могла начаться; сборочного скрипта не было вовсе, так что запускать было бы нечего и в случае успеха. Под обоими — четыре импорта:
const vsa = @import("vsa.zig");
const hybrid = @import("hybrid.zig");
const packed_vsa = @import("packed_vsa.zig");
const packed_trit = @import("packed_trit.zig");
Ни одного из этих четырёх файлов в том репозитории нет. Они в другом — в числовой библиотеке, от которой пакет зависит. А её собственный packed_vsa.zig начинался так:
const Entity = @import("knowledge_graph.zig").Entity;
Которого нет уже там. Он остался в первом репозитории.
Одну папку разделили на два репозитория, и с обеих сторон плоские относительные импорты оставили как были — теперь каждый смотрит на соседа, ушедшего на другую сторону. Итог симметричен: не компилируется ни одна половина, и каждой не хватает ровно того, что сохранила вторая.
Назвать стоит другое — почему это держалось так долго. Именно невозможность собраться мешала любой половине сообщить о пропаже другой. Висячий импорт находит компилятор; компилятор работает на пакете, который собирается; не собирался ни один. Дефект отключил единственный прибор, способный его увидеть, и потому оставался спящим — не «нашли поздно, потому что тонко», а не нашли вообще, потому что ничто наблюдающее не могло запуститься.
Всплыло случайно. Я экспортировал в числовой библиотеке постороннее объявление, и экспорт заставил компилятор проанализировать файл, которого он не анализировал никогда. Тут же выпали ещё четыре сломанных пути, и один из них назвал другой репозиторий.
У соседнего пакета в манифесте было вот это:
.url = "…/zig-golden-float/archive/refs/heads/main.tar.gz",
.hash = "golden_float-0.2.0-h7LKhdEX…",
Подвижная ссылка и хеш содержимого. Апстрим был на 2.1.0. Привязка была сломана с первого же мержа после её написания, а отказ приходится на загрузку — до того, как компилятор что-либо прочтёт, — поэтому выглядит она как несобираемый пакет, а не как устаревшая зависимость.
Я перепривязал, стало зелено. Потом слил один коммит вверху — и оно сломалось снова, в тот же час. Этот второй слом и есть ценное: он превращает довод в демонстрацию. Ссылка, которая продолжает двигаться, рядом с хешем, который продолжает утверждать, — это не привязка, а отложенный отказ. Оба пакета теперь привязаны к архиву коммита.
На прошлой неделе я добавил шаг, который отвергает набор тестов, если выполнилось меньше двухсот: набор, не выполнивший ничего, тоже завершается нулём. Шаг упал. Набор был зелёным.
Шаг звал zig test src/root.zig напрямую, минуя link_libc, который выставляет сборочный скрипт. На Linux это немедленная ошибка компиляции — значит, счётчик не распарсился, значит, гейт объявил набор пустым, тогда как шагом выше только что успешно отработали 253 теста. Гейт перестал мерить артефакт и начал мерить собственный способ до него добраться.
Починка — один запуск, прочитанный дважды: выполнить zig build test, пропустить через tee, взять код возврата из запуска и счётчик из того же вывода. Проверка, которая добывает свой вход маршрутом, отличным от сборочного, рано или поздно доложит о маршруте.
Сборка библиотеки вскрыла дефект уже в ней. Zig 0.15 сделал File.writer принимающим буфер. Процедура сохранения была написана под небуферизованный интерфейс — и после смены типа она по-прежнему компилировалась, выполнялась и возвращала успех. Хвост каждого записанного файла оставался в памяти.
То есть save() докладывал, что записал граф, который load() прочитать не мог, — и вызов, сообщающий об успехе, есть тот же вызов, который теряет данные. Ничто в системе типов не требует сброса буфера; первым, кто в состоянии заметить, оказывается читатель — и только если его кто-нибудь запустит.
Когда два бинарника не собрались, я написал в pull request, что это артефакты моего локального 0.16 против целевой 0.15.2 — std.io.getStdOut, ArrayList.init, реорганизация std.fs. На этой неделе я уже трижды оказывался прав именно так, чем, видимо, и объясняется четвёртый раз.
CI на 0.15.2 выдал те же ошибки. Они настоящие: бинарники написаны под 0.14 и не мигрированы. Я исправил это в следующем сообщении коммита и завёл миграцию отдельным issue. Публикую, а не правлю тихо, потому что механизм конкретный и я ожидаю его повторения: диагноз, оказавшийся верным несколько раз подряд, перестают проверять и начинают предполагать.
| Было | Стало | |
|---|---|---|
| build.zig | не существовал | модуль библиотеки, тест-цель на корень |
| манифест | без отпечатка, зависимость без хеша | ZON, отпечаток, привязка к коммиту |
| четыре импорта | файлы в другом репозитории | модуль зависимости |
| воркфлоу | нет | zig build + zig build test на 0.15.2 |
| тесты | незапускаемы | 7 прошли |
Семь тестов — немного, и я предпочту сказать это прямо, чем позволить зелёному означать больше, чем он означает. Изменилось узкое и стоит назвать точно: пакет, от которого никто не мог зависеть, теперь пригоден в зависимости, а компилятор впервые прочитал его исходники.
kg_cli и kg_server — это миграция с Zig 0.14 на 0.15 примерно по 1300 строкам; заведена как #3, здесь не делалась, и до неё эти два инструмента остаются тем, чем были всегда: незапускаемыми.Поработаем вместе
Я аудирую RTL и строю независимые побитово точные модели, затем провожу результат через синтез и, когда это полезно, проверяю на плате Artix-7. Первый модуль проверки — бесплатно.