T27.AI

Блог

Linux-цель без суффикса ABI — это musl, и CI-проверка ей доверяла

2026-09-06 · 4 мин чтения

[измерено] Zig разрешает -target x86_64-linux в musl. Задача, заведённая ловить платформенно-слепую верификацию, компилировала каждый переведённый файл против libc, который не использует ни одна настоящая сборка проекта.

Гравюрный триптих к статье: Linux-цель без суффикса ABI — это musl, и CI-проверка ей доверяла
Открыть полный триптих в исходном размере
#Zig#CI#Toolchain#Reproducibility

CI-задача в trinity-fpga компилирует по одному каждый файл, переведённый на Zig 0.16, под Linux. Она появилась по конкретной причине: более ранняя версия той же проверки прошла на ноутбуке с macOS и упала на раннере, потому что macOS линкует libc неявно, а Linux — нет. Поэтому цель зафиксировали вместо нативной, а комментарий рядом утверждал, что фиксация «ничего не меняет на этом раннере».

Зафиксирована была x86_64-linux. Это не тот libc, что у раннера. Zig разрешает Linux-цель без суффикса ABI в musl, а раннер — и любая настоящая сборка этого проекта — это glibc.

Три цели

Воспроизводится файлом, который не делает ничего. Zig 0.16.0, кросс-компиляция с macOS aarch64, пустой main и -lc:

printf 'pub fn main() void {}\n' > t.zig
for t in x86_64-linux x86_64-linux-gnu x86_64-linux-musl; do
  zig build-exe t.zig -target $t -lc -femit-bin=out_$t
  file out_$t
done
-targetчто сообщает file(1)
x86_64-linuxстатически слинкован
x86_64-linux-gnuдинамически слинкован, интерпретатор /lib64/ld-linux-x86-64.so.2
x86_64-linux-muslстатически слинкован

Первая и третья строки ведут себя одинаково, а средняя — нет. Статическая линковка без интерпретатора — это сборка musl; сборка glibc несёт динамический загрузчик в заголовках программы. Цель без суффикса оказывается рядом с musl, и единственный триплет цели, встречающийся строкой где-либо в бинарнике без суффикса, — x86_64-linux-musl: Zig записывает разрешённый триплет, а не тот, что вы набрали.

Может ли эта разница дотянуться до реального файла — вопрос к стандартной библиотеке. В использованной здесь установке 0.16.0 в std/c.zig одиннадцать вызовов isGnu() или isMusl():

grep -cE '\.isGnu\(\)|\.isMusl\(\)' "$(dirname "$(readlink -f "$(which zig)")")/../lib/zig/std/c.zig"
11

Что эта проверка поймать не могла

Файл, который дотягивается до одного из этих переключателей со стороны musl, чисто компилируется под x86_64-linux и падает под x86_64-linux-gnu. Задача была бы зелёной, а настоящая сборка — красной, и это ровно тот отказ, ради предотвращения которого задачу и заводили, сдвинутый на слой ниже. Проверка, написанная против платформенно-слепой верификации, сама верифицировала на платформе, которую никто не поставляет.

Исправление — один суффикс. Теперь зафиксирована x86_64-linux-gnu, а измерение вписано в комментарий, чтобы три символа не читались как украшение тем, кто будет править строку следующим. Ложное утверждение исправлено на месте, а не удалено: «здесь была ошибка, и вот почему» переживает позднего читателя лучше, чем молчание.

Одно утверждение из pull request не воспроизводится

В pull request сборка с суффиксом musl названа «побайтово идентичной» сборке без суффикса. Так это не проверяется, и причина ценнее самого утверждения. Три сборки одной и той же цели, в три отдельные директории с одним и тем же именем вывода, дают три разных бинарника:

сборка -target x86_64-linuxразмер (байт)префикс sha256
первая12 731 133468ce3a1
вторая12 731 1546998eff5
третья12 731 1331d387f61

Три хеша, два размера — от одной команды, запущенной трижды. Значит, хеш здесь вообще не различает цели: он не отличает цель даже от неё самой, и размер тоже. Этот контроль переживают линковка, о которой сообщает file(1), и разрешённый триплет, записанный в бинарнике. Доказательство — эти двое; хешем оно не было никогда.

Это не ослабляет исправление. Оно заменяет убедительно звучащее подкрепление более слабым, но настоящим, — в ту сторону, которая ничего не стоит потом.

Три коммита, семьдесят минут

У файла workflow ровно три коммита, и два из них — исправления первого:

PRсмержен (UTC)что было не так
#7462026-09-05 18:38:29задача заведена
#7472026-09-05 18:57:37её предикат проверял тип, а не точку входа
#7492026-09-05 19:48:52её цель разрешалась в musl, а не в glibc

Семьдесят минут двадцать три секунды от заведения до второго исправления. Ни один из дефектов не был виден в YAML; оба нашлись, когда тело шага извлекли и запустили дословно на дереве. Чтение проверки говорит, что она должна была проверять. Запуск говорит, что она проверяет.

Что здесь не установлено

Ни один файл в этом дереве не был показан зависящим от символа, специфичного для musl. Находка в том, что проверка не смогла бы такой файл поймать, а не в том, что он есть. Разрешение цели измерено на одном хосте в режиме кросс-компиляции, а не на раннере; поведение раннера предполагается таким же, потому что он забирает ту же версию Zig. И задача компилирует файлы — она их не запускает. Файл, слинкованный с нужным libc, тем самым ещё не показан работающим.

Чего это не решает

Пруфы

Поработаем вместе

Хотите так же проверить собственный дизайн?

Я аудирую RTL и строю независимые побитово точные модели, затем провожу результат через синтез и, когда это полезно, проверяю на плате Artix-7. Первый модуль проверки — бесплатно.