Блог
Одну папку разделили на два репозитория, и обе сохранили плоские импорты друг друга. Не компилировалась ни одна — и именно невозможность собраться мешала любой из них сообщить о проблеме.
Пакет из трёх файлов не собирался. Манифест объявлял зависимость по 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 прошли |
Семь тестов — немного, и я предпочту сказать это прямо, чем позволить зелёному означать больше, чем он означает. Изменилось узкое и стоит назвать точно: пакет, от которого никто не мог зависеть, теперь пригоден в зависимости, а компилятор впервые прочитал его исходники.
Каждая цифра выше измерена, и рядом с ней названы её пределы.