Замер собранного артефакта врёт молча: make не пересобирает, а отвечает «nothing to be done»
Вопрос «что умеет бинарник» кажется решаемым запуском: ./bootstrap/flang --help печатает список команд, список читается глазами, замер готов. Так делать нельзя, и цена ошибки уже измерена — двое подряд намерили вчерашний день и записали в роадмап, что команды run нет, хотя она есть.
Причина в том, что бинарник — продукт сборки, лежащий рядом с исходниками (так печатает Makefile бэкенда C, отдельного каталога сборки он не знает). В рабочем дереве он остаётся от прошлого прогона и по времени новее .c, из которых собран. make сравнивает времена, видит, что цель свежее зависимостей, и говорит «nothing to be done» — успешно, кодом 0. Ничего не пересобрав.
Дальше замер идёт по бинарнику, отставшему на столько, сколько прошло с прошлой сборки. Он отвечает на --help честно — про себя. Про дерево он не отвечает ничего.
Правило: артефакт меряется в каталоге, где его нет.
git archive HEAD | tar -x -C пустой/каталог
make -C пустой/каталог/bootstrap
Годится и rm -f bootstrap/flang перед make, но выгрузка строже: она заодно снимает вопрос, не подмешалось ли в замер что-то незакоммиченное.
У этой беды есть двойник, и он опаснее. Замер может идти по чистой сборке и всё равно врать — если дерево не то. Рабочая копия, чей origin смотрит на чужой клон, отстаёт молча: git fetch отрабатывает успешно, origin/main остаётся на старом коммите, и чистая выгрузка из него собирается в честный вчерашний бинарник. Отличить это от первого случая можно только одним способом: назвать коммит, на котором мерили. Замер без коммита неповторим.
Это тот же класс, что checks-that-stopped-comparing, только сравнение выключает не правка проверки и не перенос файла, а время файла или адрес удалённого. Общее у всех — зелёный ответ там, где сравнения не было.
Чем подтверждено. 18 августа 2026. На f56409b чистая сборка даёт бинарник без run — «неизвестная команда»; на b30e4edd чистая сборка даёт бинарник с run, и flang run … --function Удвоить --args '{"н":21}' печатает 42 без Node и без cc. Оба замера верны, деревья разные. Разошлись они потому, что origin рабочей копии смотрел на клон, застрявший на f56409b.
Чем ограничено. Про продукты сборки в дереве и про отставшие удалённые. Числа, которые меряются по исходникам, этой бедой не задеты напрямую — но на чужом дереве соврут вместе с ним: на f56409b строк на flang 100 208, на b30e4edd — 116 303, и обе цифры точны.
Третий вид той же беды: артефакт переписан ПОД работающей проверкой. Здесь оба замера свежие, а врёт сравнение — потому что оно захватило две редакции сразу. flang/test/self-bootstrap.test.mjs идёт около двенадцати минут и всё это время сравнивает вывод компилятора с файлами bootstrap/ НА ДИСКЕ. Запустишь в это же время node scripts/bootstrap-c.mjs — и половина сравнения поедет на новую редакцию, а половина останется на старой. Отказ выглядит в точности как сломанная неподвижная точка:
компилятор, напечатанный самим собой, разошёлся со свидетелем: compiler_flang.h, строка 30800 свидетель: " Функция flang «Неотрицательно»." flang₁: " Функция flang «Упоминает имена»."
Ни одна из сторон не ошиблась: flang₁ напечатал текущее дерево, а файл на диске был от прошлой перепечатки. Проверяется это за секунды и БЕЗ записи:
node scripts/bootstrap-c.mjs --check
— он отвечает «точка раскрутки bootstrap/ совпадает с печатью: 7 файлов, 13 661 291 байт» и кодом 0, ничего не переписывая. Правило: перепечатка — запись, и её нельзя делать, пока что-то читает bootstrap/; а половину вопроса «совпадает ли закоммиченное с печатью» надо задавать --check, а не двенадцатиминутной проверкой.
Чем подтверждено. 19 августа 2026, ветка work/klass-nomera. Прогон self-bootstrap дал 30 зелёных и 1 красный на шаге неподвижной точки; в закоммиченном заголовке строка 30800 читается ровно так, как её напечатал flang₁, а --check отвечает кодом 0. То есть красный был мой собственный, а не дерева.
Связано: checks-that-stopped-comparing, byte-for-byte-comparison, a-shared-log-path-silently-replaces-your-run