Мой PR в Bun пережил язык, на котором был написан

Продолжение истории: мой фикс на Zig для bun create закрыл бот — потому что Bun переехал на Rust. Я разобрался, что случилось на самом деле, переписал фикс на Rust, повоевал с тулчейном на NixOS и упёрся в настоящее узкое место Bun — не в код, а в ревью.

Терминальная превьюшка: PR #29089 закрыт как устаревший до перехода на Rust, src/cli.zig переехал в runtime/cli, переоткрыт как Rust-PR #32954

Это продолжение поста Как сломанная команда Nuxt привела к багу в Bun CLI.

Тот пост закончился открытым PR в Bun. К тому моменту я нашёл настоящий баг — bun create <template> -- <args> передавал дальше сам разделитель --, вместо того чтобы его проглотить, — свёл его к маленькому примеру, завёл oven-sh/bun#29087 и отправил исправление на Zig: oven-sh/bun#29089.

А потом PR закрыли. Не человек — бот. И причина, которую он назвал, оказалась интереснее самого бага.

PR закрыл бот

Бот закрыл его автоматически с комментарием, который, если пересказать своими словами, звучал так:

Закрываю как устаревший: этот PR появился ещё до перехода на Rust. Все файлы в src/, которые он меняет, с тех пор удалены или перенесены в main.

Сначала мне показалось, что это чушь. Я открыл свою локальную копию репозитория — и src/cli.zig, единственный файл, который трогал мой PR, был на месте. Никакого Rust и в помине. Bun написан на Zig. Бот явно что-то выдумал.

Только вот нет. Моя ветка main отставала на сотни коммитов. Когда я подтянул настоящий, свежий main, файла src/cli.zig там уже не было.

Теперь Bun написан на Rust

Вот что я полностью прозевал: Bun переписывает своё ядро с Zig на Rust. В текущем main (и в canary-сборках) собирается и поставляется именно Rust-версия.

Стоило присмотреться — и следы были по всему репозиторию:

  • src/cli теперь стал симлинком на runtime/cli;
  • логика bun create, которую я правил на Zig, переехала в Rust — в src/runtime/cli/mod.rs, create_command.rs и bunx_command.rs;
  • вся Rust-часть — это один Cargo-воркспейс примерно из двухсот крейтов;
  • появились файлы .rs, clippy.toml и целые скрипты clippy-loop там, где раньше жил Zig.

То есть прав был бот, а не я. Мой PR нельзя было ни перебазировать, ни переоткрыть по-человечески — файла, который он редактировал, больше не существовало. Вывод дошёл моментально: сначала подтяни апстрим, потом доверяй своей рабочей копии. Я спорил с ботом, держа в голове картину мира многомесячной давности.

Баг пережил переписывание

И вот тут повезло. Когда код переписывают, баги переезжают вместе с ним.

Я открыл новую логику передачи аргументов в src/runtime/cli/mod.rs — и она повторяла старый Zig один в один: тот же цикл, который отправлял в bunx всё подряд после имени шаблона, включая ведущий --. Переписывание аккуратно перенесло баг через границу языка.

Значит, исправление по-прежнему нужно — просто на другом языке. Я переписал его на Rust:

// убрать один ведущий `--`; проглотить `--bun` перед ним;
// сохранить второй `--` и любой `--bun` после разделителя как обычные аргументы

Поведение ровно такое же, как в исходном фиксе на Zig:

  • убирает один ведущий -- перед передачей аргументов в create-скрипт;
  • проглатывает собственный флаг обёртки --bun, если он стоит до разделителя;
  • сохраняет второй -- и --bun после разделителя как обычные аргументы.

Регрессионный тест из старого PR переехал почти без правок — он поднимает локальный реестр пакетов, запускает bun create против пакета, чей бинарник печатает свои аргументы, и проверяет все варианты с разделителем. Новый PR здесь: oven-sh/bun#32954.

Собрать Bun на NixOS — отдельное приключение

В первом посте я отделался расплывчатым «проблемы с тулчейном на NixOS». В этот раз я честно попытался собрать проект и прогнать тест — и упёрся в стену в трёх разных местах. Записываю, потому что любой на NixOS наступит на те же грабли:

  1. zstd. Отладочная сборка идёт с -gz=zstd (отладочные секции, сжатые zstd), но clang_21 из nixpkgs собран без поддержки zstd. Везде это предупреждение — кроме предкомпилированного заголовка, где включён -Werror, так что там оно становится ошибкой. Помогает переключение на -gz=zlib.
  2. _FORTIFY_SOURCE. Обёртка компилятора в Nix насильно добавляет -D_FORTIFY_SOURCE=2, но отладочная сборка идёт с -O0, а fortify требует оптимизации. Получается предупреждение, которое -Werror снова превращает в ошибку. Лечится исключением fortify из NIX_HARDENING_ENABLE.
  3. Nightly-версия Rust. Вот это быстро обойти не вышло. Bun жёстко фиксирует конкретную nightly-сборку Rust и использует -Zbuild-std. А у меня на машине был только стабильный cargo/rustc из Nix и не было rustup, чтобы поставить нужный nightly. Си-плюс-плюсная половина собралась, а Rust-овая — нет.

Так что собрать Bun целиком локально я снова не смог. Я прямо написал об этом в описании PR и положился на CI. Уж лучше отправить PR с честным «локально прогнать не смог, вот почему», чем делать вид, будто был зелёный прогон, которого не было.

Автоматическое ревью

CI у Bun запускает на каждый PR автоматическое ревью (CodeRabbit). Бот оставил несколько замечаний — и вот тут, по-моему, и начинается самое интересное, потому что слепо применять всё, что говорит бот, не стоит.

Два замечания были по делу: подрезать слишком длинный комментарий и проверять код возврата подпроцесса в тесте, чтобы тот не проходил молча при ненулевом выходе. Оба взял.

Одно отклонил. Бот утверждал, что обёртка должна обрабатывать -b (короткую форму --bun) так же, как --bun. Я проверил: -b и правда настоящий псевдоним в bunx. Но сделать это правильно значило перелопатить существующую логику разбора аргументов, которую мой патч вообще не трогал; к тому же это выходило за рамки исходной задачи, а -b и так работает — bunx понимает его дальше по цепочке. Поэтому я ответил с объяснением и предложил вынести это в отдельный PR. Даже сам промпт CodeRabbit говорит: «чини только то, что действительно осталось багом, остальное пропусти с коротким объяснением». Ровно этот же инстинкт стоит применять к любой автоматической подсказке — в том числе к тем, что я пишу с Claude Code.

Настоящее узкое место — не код

А теперь неприятная часть. Само исправление маленькое. Сложность — добиться, чтобы на него посмотрел живой человек.

Поскольку мой PR из форка, CI на нём даже не запускается автоматически — сначала его должен разблокировать мейнтейнер. И это ещё мягкий барьер. Если провести немного времени в Discord-канале Bun, замечаешь повторяющийся сюжет: у внешних контрибьюторов корректные PR, которые один раз отревьюили и где уже внесли правки, висят месяцами. Восемь месяцев. Девять. Люди пишут мейнтейнерам на почту, отмечают их на GitHub, раз в пару недель напоминают о себе в Discord — а в ответ только активность ботов.

Это не злой умысел, и я хочу быть честным: Bun делает очень маленькая команда, которая движется крайне быстро, а переписать весь рантайм с Zig на Rust — это огромная работа, которая по понятным причинам съедает все силы на проверку чужого кода. Основатель проекта по-прежнему коммитит каждый день. Но для внешнего контрибьютора сейчас всё выглядит так: с твоим PR взаимодействует автоматика — robobun и компания, — а внутренние задачи (вполне разумно) идут вперёд очереди из community-исправлений.

Если соберёшься контрибьютить в Bun сегодня — иди туда, держа это в голове. Это сильно меняет подход.

Как сейчас реально контрибьютить в Bun

Что я сказал бы себе перед стартом:

  • Целься в Rust-код, а не в Zig. Рантайм, который поставляется, лежит в src/ как Cargo-воркспейс. bun create живёт в src/runtime/cli/. Сначала найди тот слой, который реально компилируется, а уже потом пиши хоть строчку.
  • Всегда сначала подтягивай апстрим. main движется так быстро, что недельной давности копия может оказаться уже другим кодом. Я выучил это, споря с ботом.
  • Своди к минимальному примеру. Крошечный пакет, печатающий свои аргументы, сделал баг неоспоримым, а исправление — очевидным. Когда ревью в дефиците, это важно вдвойне: проверяющий должен убедиться в проблеме за тридцать секунд.
  • Всегда добавляй регрессионный тест. Это разница между «поверь мне» и «вот доказательство, и доказательство, что баг не вернётся».
  • Честно пиши в PR о том, что не смог проверить. Я не собрал проект локально — и так и написал. Проверяющему это полезнее ложной уверенности.
  • Закладывай задержку и напоминай о себе вежливо. Понятное описание PR, связанные задачи и редкое уважительное напоминание — это всё, что в твоих силах. Не спамь.
  • Используй ИИ как штурмана, а не как оракула. Claude Code реально помог перенести готовое исправление на язык, на котором я не пишу каждый день, — но только после того, как я сам понял суть. Он не заменил понимание задачи и не должен его заменять.

Чем всё закончилось (пока)

Исправление теперь написано дважды, на двух разных языках, ради одного и того же однострочного поведения: bun create должен убирать первый -- перед передачей аргументов дальше. Rust-PR открыт. CI заблокирован и ждёт мейнтейнера. Три задачи, которые он закрывает (#29087, #20314, #6566), всё ещё открыты.

Бот был прав, что мой PR появился до переписывания. Он только не уточнил, что баг появился ещё раньше — и пережил его. Так что я переписал исправление на Rust и снова встал в очередь.

Теперь, как и все, жду.