Skip to content

RFC-037: Промышленная схема распространения — упаковка компилятора/тулчейна на базе cargo-dist ​

Настоящий RFC дополняет RFC-014b: Система сборки и распространение бинарников. RFC-014b определяет, как менеджер пакетов YaoXiang собирает и распространяет сторонние пакеты; настоящий RFC определяет, как сам компилятор/тулчейн YaoXiang упаковывается и распространяется.

Резюме ​

Использовать cargo-dist (инструмент распространения бинарников из экосистемы Rust) для оркестрации кросс-платформенной сборки, а собственные скрипты — для структуры дистрибутива. Два ключевых обязательства: дистрибутив физически содержит каталог исходников стандартной библиотеки (пользователь читает его так же напрямую, как Python Lib/), а также динамически линкуемая на всех платформах общая библиотека Z3 поставляется вместе с пакетом. Модель команд — разделение на «переднюю дверь» и движок: основная команда yx (маленький фасад со встроенным управлением версиями — аналог rustup/Go GOTOOLCHAIN), движок yaoxiang-rs (переименование нынешнего монолита yaoxiang), что позволяет избежать раскола экосистемы, с которым столкнулись Python/Node, задним числом закрывая проблему через nvm/pdm. Двухуровневая установка: стандартный канал выравнивается с Go/Zig — дистрибутив и есть продукт, распаковка + PATH; дружественный канал — установка одной строкой (Linux apt / curl | sh, Windows irm | iex / Inno exe-мастер). Решает проблемы отсутствия libz3.dll, недоступности стандартной библиотеки для пользователя, дублирования CI-скриптов.

Мотивация ​

Зачем нужна эта возможность? ​

Скачавший YaoXiang пользователь должен иметь возможность сразу начать работу без каких-либо дополнительных шагов; стандартная библиотека должна быть напрямую читаемой для пользователя, а не чёрным ящиком внутри бинарника.

Текущие проблемы ​

Проблема 1: Пользователи Windows не могут запустить после скачивания ​

Текущий Release загружает только yaoxiang.exe, но libz3.dll не упаковывается. Пользователь на Windows получает ошибку при двойном клике:

The code execution cannot proceed because libz3.dll was not found.

Это блокирующий баг — пользователь не проходит даже первый шаг.

Проблема 2: Артефакт Release — это лишь одиночный exe, стандартная библиотека недоступна для пользователя ​

Текущее положение — тройной разрыв:

  • Артефакт Release — голый бинарник, стандартная библиотека не поставляется с дистрибутивом
  • Цепочка поиска интерфейсных файлов LSP фактически разорвана: при вызове find_std_interface_file не передаётся каталог проекта (ищется только глобально в ~/.yaoxiang/std/, но нет процедуры, его заполняющей); package init пишет в .yaoxiang/std, которого нет в цепочке поиска
  • Исходники стандартной библиотеки (уровень .yx) и интерфейсное представление (уровень native) — полностью чёрный ящик для пользователя

Индустриальный подход: пользователь просто открывает каталог стандартной библиотеки и читает исходники — точно как Python Lib/ — физическое наличие каталога std в дистрибутиве — жёсткое требование настоящего решения (решено).

Проблема 3: Дублирование ручных CI-скриптов ​

В настоящее время поддерживается несколько сборочных пайплайнов:

ФайлНазначениеСтрок
_build-platforms.ymlКросс-платформенная сборка~255
release.ymlВыпуск версий~189
nightly.ymlЕжедневные сборки~173
scripts/build/setup.issInno Setup инсталлятор~250
Итого~870

Большая часть дублируется (установка Rust → кэш → сборка → переименование → загрузка), для каждой платформы пишется заново.

Проблема 4: Жёстко зашитый номер версии в Inno Setup ​

В setup.iss поле MyAppVersion жёстко зашито как 0.7.0, при сборке подменяется через sed. Рано или поздно это приведёт к сбою.

Проблема 5: Размытость границ с RFC-014b ​

RFC-014b определяет «механизм сборки и распространения пакетов YaoXiang» (т.е. секции [build] и [binaries] в yaoxiang.toml), но не покрывает «как именно выпускается сам компилятор YaoXiang». Настоящий RFC закрывает этот пробел.

Предложение ​

Основной дизайн ​

cargo-dist берёт на себя только слой оркестрации сборки; структура пакетов и инсталляторы — собственные. Разделение обязанностей:

Обязанности cargo-dist (слой оркестрации сборки):
  ├── Кросс-платформенная компиляция (5 таргетов)
  └── Генерация архивов и контрольных сумм
  (Родные инсталляторы и npm wrapper отброшены — их допущение о плоском бинарнике конфликтует со структурой bin/+lib/)

build.rs продолжает отвечать за:
  └── Загрузку/линковку Z3 (динамическая линковка + rpath на всех платформах)

Собственные скрипты YaoXiang:
  ├── package-dist.sh — пересборка структуры пакета (bin/ + lib/), добавление общей библиотеки,
  │   заполнение каталога std (предварительно сгенерированные в репозитории интерфейсные представления +
  │   исходники уровня .yx), пересчёт контрольных сумм
  └── Inno Setup — Windows-мастер установки (существующий актив; раскладывает полную структуру каталогов)

Модель команд (разделение на «переднюю дверь» и движок):
  ├── yx — передняя дверь (новый маленький crate): разрешение версий + диспетчеризация;
  │   оставлены только глаголы toolchain/self, остальные прозрачно транслируются
  └── yaoxiang-rs — движок (переименование нынешнего монолита yaoxiang): подкоманды
       компиляции/запуска/управления пакетами/fmt/lsp

Способы установки (двухуровневые):
  ├── Стандартный канал (модель Go/Zig): дистрибутив и есть продукт, распаковка + PATH
  └── Дружественный канал (модель Rust): установка одной строкой + управление версиями
      (встроено в переднюю дверь yx)
      ├── Linux: apt (собственный deb-репозиторий, системная установка) / curl … | sh (установка всего пакета одной строкой)
      ├── Windows: irm … | iex (установка всего пакета одной строкой) / Inno Setup мастер (существующий актив, системная установка)
      └── macOS: curl … | sh (brew оставлен сообществу homebrew-core)

Структура каталогов дистрибутива (решено: физически содержит исходники стандартной библиотеки) ​

Пользователь должен иметь возможность читать стандартную библиотеку так же напрямую, как Python Lib/ — наличие каталога std в дистрибутиве — жёсткое требование, а не деталь упаковки. С Z3 то же самое: как внешняя система, каталожное распространение общей библиотеки — её естественная форма; запихнуть .so внутрь exe или положить рядом и динамически линковать — в плане «всё равно идёт с дистрибутивом» эквивалентно, но второй вариант сохраняет заменяемость.

Для каждой платформы дистрибутив пересобирается скриптом package-dist.sh после сборки cargo-dist:

yaoxiang-{version}-{target}.tar.gz / .zip     (портативный, готовый к использованию: после распаковки
                                                запускается прямо из bin/)
├── bin/
│   ├── yx                            # Передняя дверь (или yx.exe)
│   ├── yaoxiang-rs                   # Движок (или yaoxiang-rs.exe)
│   └── libz3.so / libz3.dylib / libz3.dll
├── lib/
│   └── yaoxiang/
│       └── std/                      # Пользователь может читать напрямую (модель Python Lib/)
│           ├── io.yx                 # native-модули: предварительно сгенерированные в репозитории
│           │                         # интерфейсные представления
│           ├── math.yx
│           ├── test.yx               # Уровень .yx: копия реальных исходников из репозитория
│           └── ...
├── README.md
└── LICENSE

Установка = распаковка в любой каталог + добавление bin/ в PATH (модель Go /usr/local/go/bin; распаковка в ~/.yaoxiang/ — частый выбор). На Windows то же самое делает мастер Inno Setup (по умолчанию Program Files). При портативной распаковке передняя дверь yx не имеет состояния в ~/.yaoxiang, откатывается к соседнему yaoxiang-rs — поведение совпадает с управляемой установкой; rpath движка и поиск std относительно exe не меняются от наличия передней двери.

Поддержка платформ ​

Платформаtarget tripleПримечание
Linux x86_64x86_64-unknown-linux-gnuОсновная платформа
Linux ARM64aarch64-unknown-linux-gnuКросс-компиляция в CI
macOS x86_64x86_64-apple-darwinIntel Mac
macOS ARM64aarch64-apple-darwinApple Silicon
Windows x86_64x86_64-pc-windows-msvcОсновная платформа

Всего 5 таргетов. Windows ARM64 временно не поддерживается (у Z3 нет официальных прекомпилированных пакетов для ARM64).

Стратегия распространения Z3 ​

Динамическая линковка на всех платформах (подтверждено пересмотром):

ПлатформаИзменениеАртефакт
LinuxБыло статической → стало динамическойlibz3.so
macOSБыло статической → стало динамическойlibz3.dylib
WindowsБез измененийlibz3.dll
wasm32Без изменений (статическая линковка)Встроен .a

Обоснование:

  • Единообразие — все три платформы ведут себя одинаково, без платформенных исключений
  • Это внешняя библиотека, она и должна распространяться как общая библиотека. Так делают Python (python3.dll+DLLs/lib*.dll), Node (node+lib/)
  • Обновление Z3 не требует ожидания новой версии компилятора — достаточно заменить .so/.dylib/.dll
  • Меньший размер бинарника — Z3 не маленькая, статическая линковка раздувает exe на несколько мегабайт

У динамической линковки есть необходимое сопровождение: динамический линковщик на Linux/macOS по умолчанию не ищет в каталоге бинарника, нужно внедрить rpath, иначе «распакуй и используй» не работает (Windows по умолчанию ищет в каталоге exe, обработка не нужна). Соответствующее изменение в build.rs:

rust
// Единая динамическая линковка + rpath
fn link_z3(z3_dir: &Path) {
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
    // Раскладка Z3 в дистрибутиве не единообразна, пробуем каталоги lib/bin
    println!("cargo:rustc-link-search=native={}", lib_dir.display());
    // RFC-037: динамическая линковка на всех платформах. Общая библиотека поставляется
    // с дистрибутивом в bin/, пользователь может целиком заменить/обновить Z3
    if target_os == "windows" {
        // MSVC import-библиотека называется libz3.lib
        println!("cargo:rustc-link-lib=libz3");
    } else {
        println!("cargo:rustc-link-lib=z3");
        // Динамический линковщик по умолчанию не ищет в каталоге бинарника, нужно внедрить
        // rpath, чтобы «распакуй и используй» работало
        // (в дистрибутиве exe и libz3 лежат рядом в bin/; на Windows ищется в каталоге exe по умолчанию)
        match target_os.as_str() {
            "linux" => println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"),
            "macos" => println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path"),
            _ => {}
        }
        let cxx = if target_os == "macos" {
            "c++".to_string()
        } else {
            env::var("CXXSTDLIB").unwrap_or_else(|_| "stdc++".into())
        };
        println!("cargo:rustc-link-lib={}", cxx);
    }
}

«Статическая линковка на всех платформах» не ставится целью. Это не устранение частного случая, а попытка устранить его неправильным способом. Общая библиотека — нормальный способ распространения внешней библиотеки.

Поддержка инсталляторов ​

Сравнение способов распространения основных языковых тулчейнов (обзор на 2026-09):

ЯзыкОфициальный дистрибутивОфициальный способ установкиКто поддерживает инсталлятор
Gogo/{bin,src,pkg} tarballОфициальная документация = «скачать → распаковать → PATH»Нет (brew/apt — сообщество)
Zigzig/{bin,lib/std} tarballТо же, без официального скрипта установкиНет (homebrew-core — сообщество)
Node{bin,lib,include} tarballtar + официальный pkg/msiКоманда, сама
RustМногокомпонентный tarballrustupКоманда, сама
Crystal{bin,src,embedded} tarballdeb/rpm/tarКоманда + brew-сообщество
Deno/BunОдиночный бинарник zipОфициальный curl-скриптКоманда, сама (скрипт минимален)
GleamОдиночный бинарник cargo-distСкрипт, сгенерированный cargo-distcargo-dist

Три закономерности:

  • Ни один тулчейн с множеством файлов не использует сторонний генератор для инсталляторов — инсталляторы cargo-dist заточены под сценарий одиночного бинарника (Gleam может их использовать именно потому, что это единственный Rust-бинарник без внешних зависимостей)
  • Простейшая модель — «дистрибутив = продукт» как у Go/Zig: официальная инструкция — распаковка + PATH, ноль кода инсталлятора; дистрибутив содержит читаемые исходники std (Go src/, Zig lib/std/, Crystal src/) — это норма
  • Те, кто хотят «curl в одну строку» (Deno/Bun/rustup) — пишут скрипты сами и они почти не развиваются; brew-формулы всегда поддерживаются сообществом в homebrew-core, языковые команды не создают собственные tap (команда Crystal явно говорит, что формула — дело сообщества)

YaoXiang использует двухуровневую модель:

КаналУровеньСтатусОписание
zip / tar.gzСтандартный✅Готов к использованию (rpath + общая библиотека в том же каталоге), официальная инструкция — распаковка + PATH
yx (передняя дверь, встроенное управление версиями)Дружественный✅Аналог rustup/Go GOTOOLCHAIN: установка/переключение/обновление нескольких версий + project pin
curl ... | sh (install.sh)Дружественный✅Linux / macOS: скачивает пересобранный пакет в versions/, размещает bin/yx в корне установки, записывает версию по умолчанию
irm ... | iex (install.ps1)Дружественный✅Windows: та же логика
apt install yaoxiangДружественный✅.deb (amd64/arm64) + статический apt-репозиторий на GitHub Pages; системная установка, обновление через apt upgrade
Inno Setup exeДружественный✅Windows-мастер (существующий актив), системная установка, раскладывает полную структуру bin/+lib/
winget / .rpm / homebrew-core / npm—⏸Возможные опции: winget и brew-core поддерживаются сообществом, rpm аналогичен deb
MSI / родной инсталлятор cargo-dist / собственный brew tap—❌См. «Альтернативы»

Дружественный канал берёт за образец Rust, управление версиями встроено в переднюю дверь. Декомпозиция Rust — «скрипт-бутстрап (sh.rustup.rs) → rustup → пакеты тулчейна»: rustup сразу берёт на себя несколько версий, переключение, обновление; официальные инсталляторы Python/Node не делали этот слой, и экосистема задним числом обросла pyenv/nvm/pdm, каждый по-своему. YaoXiang применяет разделение на «переднюю дверь» и движок (Go: передняя дверь go + GOTOOLCHAIN; rustup: прокси-диспетчеризация — изоморфные формы): основная команда — yx, движок — yaoxiang-rs —

~/.yaoxiang/
├── bin/yx                  # Передняя дверь: маленький бинарник, разрешение версий + диспетчеризация
│                            # (project pin > по умолчанию > соседний движок)
├── settings.toml           # Версия по умолчанию, зеркала
└── versions/               # Версия — понятие первого уровня; <ver>/ — корень распаковки
                            # дистрибутива этой версии
    └── 0.7.14/
        ├── bin/
        │   ├── yaoxiang-rs      # Движок: подкоманды компиляции/запуска/управления пакетами/fmt/lsp
        │   └── libz3.so
        └── lib/yaoxiang/std/
  • Корень установки ~/.yaoxiang/ соответствует отраслевой практике (pyenv ~/.pyenv, nvm ~/.nvm, deno ~/.deno, bun ~/.bun, volta ~/.volta — все с единым корнем; rustup с двойным корнем ~/.cargo+~/.rustup — историческое наследие cargo, появившегося раньше rustup, не копируем), и ~/.yaoxiang уже существующее в кодовой базе пространство имён (глобальный слот отката std); поддерживается переопределение через переменную окружения YAOXIANG_HOME (прецедент RUSTUP_HOME / DENO_INSTALL, обслуживает CI и контейнеры); в Windows — %USERPROFILE%\.yaoxiang
  • Каталог версии = корень распаковки дистрибутива: versions/<ver>/ полностью изоморфен портативной распаковке и дереву установки deb; установка версии = распаковка дистрибутива — три канала без структурного расхождения
  • Командный интерфейс (по аналогии с rustup): yx toolchain install / default / update / list / uninstall, включая yx self update; остальные глаголы прозрачно транслируются в движок
  • Блокировка версий — структурная гарантия: fmt и подобные инструменты эволюционируют вместе с синтаксисом в одной версии (старый fmt не распознаёт новый синтаксис), разрешение версий делается один раз перед дверью, переключается весь набор — не существует комбинации «новый движок + старый fmt»; если в будущем fmt/LSP будут выделены в отдельные бинарники, они окажутся в bin/ той же версии
  • Project-level pin: yx-toolchain.toml (прецедент rust-toolchain.toml, следует имени команды; не в yaoxiang.toml — манифест пакета не должен навязывать версию тулчейна потребителям библиотек)
  • Точка входа бутстрапа (curl | sh / irm | iex) за один раз устанавливает последнюю стабильную версию целиком: распаковывает в versions/, размещает bin/yx в корне установки, записывает settings.toml с версией по умолчанию (эквивалентная сходимость к декомпозиции rustup «бутстрап устанавливает менеджер» — передняя дверь и движок в одном пакете, двухшаговая схема не нужна)
  • Зеркала настраиваются (settings.toml), продолжает учитываться ситуация с сетью для пользователей из КНР (та же проблема, что и с загрузкой Z3)
  • Управление версиями не нарушает инвариант самодостаточности: каждая версия — полное дерево дистрибутива, rpath и поиск std относительно exe самосогласованы внутри дерева, передняя дверь только диспетчеризует и не меняет структуру

.deb и Inno — каналы системной установки (root / Program Files, единственная версия, обновляются через apt upgrade / панель управления), обслуживают серверы, CI, сценарии чистого новичка; сосуществование с менеджером — через порядок PATH (прецедент: rustc из apt и rustup сосуществуют). Все каналы разделяют одно и то же дерево продуктов.

Раскладка .deb повторно использует то же дерево каталогов: /usr/lib/yaoxiang/ (всё дерево дистрибутива: bin/{yx,yaoxiang-rs,libz3.so} + lib/yaoxiang/std/) + символическая ссылка /usr/bin/yx на /usr/lib/yaoxiang/bin/yx — $ORIGIN вычисляется по реальному пути после резолва, после линковки всё равно попадает на libz3.so в том же каталоге, изоморфно структуре распакованного пакета. Полное название бренда остаётся в имени пакета и продукта (apt install yaoxiang, имя продукта Inno — YaoXiang), командный интерфейс — единый yx — как в Go: имя пакета golang-go, команда go. apt-репозиторий статически хостится на GitHub Pages (метаданные Packages/Release/InRelease с GPG-подписью, публикуются release CI); в перспективе — заявка в официальные репозитории Debian/Ubuntu (долгий цикл, отстающие версии, не основной путь).

Каталог стандартной библиотеки ​

Содержимое lib/yaoxiang/std/ полностью состоит из статических файлов репозитория, упаковка — чистое копирование, без точек генерации во время выполнения:

УровеньИсточникПрирода
native-модули (io/math/…)Репозиторий src/std/interfaces/*.yx — предварительно сгенерированные интерфейсные представленияПредставление сигнатур интерфейсов (реализация внутри бинарника)
Модули уровня .yx (test/… растут)Репозиторий src/std/*.yx — копия как естьРеальные исходники

Предварительно сгенерированные представления порождаются из StdModule::exports() (generate_all_interfaces() в src/std/gen_interfaces.rs), подкоманда генерации во время выполнения не заводится (решено 2026-09-10: после того как упаковка сформирована, подкоманда — лишняя поверхность интерфейса). Для синхронизации применяется паттерн «генерат в репозиторий + тестовый шлагбаум» (как таблица кодов в RFC-013): test_committed_interface_files_match_generation побайтово сравнивает предварительно сгенерированные файлы с текущей генерацией, расхождение = красный тест; исправление через точку входа bless — cargo test update_committed_interface_files -- --ignored. Логика генерации зависит от внутрикрейтовой реализации StdModule, её нельзя опустить в build.rs, поэтому шлагбаум стоит в тестах, а не на этапе сборки.

Цепочка поиска во время выполнения — в find_std_interface_file добавлен уровень поиска относительно exe:

  1. Проект .yaoxiang/vendor/std/<name>.yx (переопределение проекта, существующее)
  2. Каталог exe ../lib/yaoxiang/std/<name>.yx (новое): портативная распаковка, управляемая установка (versions/<ver>/), deb — всё единообразно попадает
  3. ~/.yaoxiang/std/<name>.yx (глобальный откат, оставлен как слот ручного переопределения)

Сейчас эта цепочка фактически разорвана (вызов LSP не передаёт каталог проекта; package init пишет в .yaoxiang/std, которого нет в цепочке), в этой работе она попутно подключается, и package init унифицированно пишет в .yaoxiang/vendor/std (соответствует каталогу vendor менеджера пакетов).

Авторитет компиляции не меняется: уровень .yx по-прежнему встраивается через include_str! (инвариант RFC-036 «версия std строго связана с бинарником» сохраняется). Каталог дистрибутива — это читаемое представление + источник разрешения LSP, а не вход компиляции — ручное изменение .yx в каталоге дистрибутива не будет принято компилятором (нужно ли открывать питоновскую семантику «правка Lib/ сразу действует», см. открытые вопросы).

Сборка Wasm ​

Остаётся отдельной, не входит в cargo-dist.

cargo-dist занимается «доставкой компилятора пользователю», wasm — это «встраиваемый онлайн-playground в сайт документации» — два совершенно разных продукта поставки.

АспектПодход
Инструмент сборкиОстаётся wasm-pack build
CI workflowОставить отдельное job в _build-wasm.yml
Момент запускаНа тот же tag-push что и release, параллельное отдельное job
Цель публикацииdocs/public/wasm/ → GitHub Pages

npm-публикация ​

ПакетСодержимоеСтатус
@yaoxiang/cliWrapper для скачивания дистрибутиваОтложено: npm wrapper cargo-dist также основан на допущении плоского артефакта, отбрасывается вместе с инсталляторами; если нужен канал npm, пишется свой wrapper (скачивает и распаковывает пересобранный пакет, та же логика что и распаковка при установке)
@yaoxiang/playgroundwasm-библиотека (JS + .wasm)Опционально, сейчас публикуется только в docs

Конфликта нет, и имена не пересекаются.

Интеграция с существующим процессом выпуска ​

Текущий release.yml: push main → check-version (пропускает, только если тег v{version} ещё не существует) → четыре ветки build / build-wasm / security / test → job release (создаёт и пушит тег + generate-commit-list.ts генерирует тело с @mentions + загружает артефакты).

Сгенерированный cargo-dist пайплайн запускается по тегу, имеет встроенные announce/publish, и не содержит шлагбаумов fmt/clippy/test/audit, формат release notes также не может нести merge commit changelog. Прямая полная замена сломала бы существующий ритуал выпуска (PR → весь CI зелёный → bump → merge commit = changelog).

Принцип интеграции: Триггер и шлагбаумы остаются как есть, сборку отдать cargo dist build, публикацию оставить как есть.

  1. Три job check-version / security / test остаются как есть (триггер — push main, шлагбаумы перед созданием тега)
  2. После прохождения всех трёх job release создаёт и пушит тег v{version} (без изменений)
  3. tag-push запускает новый dist-release.yml: plan job — dist вычисляет матрицу runner/системных зависимостей → cargo dist build (5 таргетов) → package-dist.sh пересобирает по каждому таргету → Inno Setup job (берёт пересобранный Windows-пакет, собирает мастер с инжекцией /DMyAppVersion=, повторной компиляции нет) → _build-wasm.yml (параллельное job)
  4. publish job: generate-commit-list.ts генерирует тело (текущий скрипт переиспользуется) → добавляет загрузку пересобранных пакетов + .sha256 + .deb + wasm + Setup exe; отдельный publish-apt job публикует метаданные apt-репозитория на GitHub Pages (автоматически пропускается, если secrets.APT_GPG_KEY не настроен, не влияет на остальные каналы)

Nightly-публикация ​

cargo-dist не имеет встроенной поддержки nightly (axodotdev#1143, всё ещё открытый feature request).

Остаётся текущая схема cron + перезапись тега, часть сборки меняется с _build-platforms.yml на cargo dist build — это по сути команда cargo, можно вызывать прямо из nightly.yml. Без повторного использования workflow (задуманное uses: ./release.yml не работает: у используемой стороны должен быть триггер workflow_call, а workflow cargo-dist привязан к тегу, сборка и публикация в нём связаны):

yaml
# nightly.yml (после миграции)
on:
  schedule:
    - cron: '17 22 * * *'
jobs:
  build: # cargo dist build + package-dist.sh (тот же набор, что и для релиза)
  publish: # Текущее сохранено: создать/перенести nightly тег → перезаписать GitHub Pre-release

Конфигурация cargo-dist (уже в виде dist-workspace.toml в репозитории) ​

toml
[workspace]
members = ["cargo:.", "cargo:tools/yx"]

# Конфигурация 'dist'
[dist]
# Фиксируем версию dist (синтаксис SemVer из Cargo.toml)
cargo-dist-version = "0.32.0"
ci = "github"
# Инсталляторы все свои, cargo-dist делает только сборку + архивы + контрольные суммы
installers = []
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
# Сгенерированный workflow намеренно модифицирован (пересборка package-dist.sh + собственный
# блок публикации, RFC-037), не отказываем в сборке из-за расхождения с шаблоном generate
allow-dirty = ["ci"]

Это фактическая конфигурация в репозитории (сгенерирована cargo dist init и доработана); cargo-dist-version зафиксирован на 0.32.0, сгенерированный workflow завендорен в репозиторий и проходит review, не подтягивается во время выполнения. Профиль сборки инжектируется через [profile.dist] в корневой Cargo.toml из init (inherits release, lto=thin), оба бинарника сохраняются в target/<triple>/dist/ для пересборки скриптом.

package-dist.sh (уже в репозитории) ​

За основу берётся scripts/release/package-dist.sh в репозитории, ключевые моменты:

  • Оба бинарника берутся прямо из выходного каталога сборки cargo dist build — target/<triple>/dist/ (profile=dist); плоский однобинарниковый архив, который сам cargo-dist производит, не является продуктом поставки, пересобранный одноимённый пакет в target/distrib/ напрямую перезаписывается
  • Общая библиотека Z3 также берётся из выходного каталога сборки — build.rs при линковке уже выбрал подходящую общую библиотеку платформы и скопировал её на диск (copy_shared_lib), единый источник: скрипт упаковки не знает и не должен знать версию Z3 и именование каталогов платформы; текст лицензии Z3 сохраняется рядом с библиотекой и включается в пакет (обязательство MIT-распространения), на стороне macOS при упаковке install_name dylib приводится к @rpath и бинарник ad-hoc переподписывается
  • Каталог std — чистое копирование: src/std/interfaces/*.yx (предварительно сгенерированные интерфейсные представления) + src/std/*.yx (реальные исходники уровня .yx)
  • Прилагаются README/LICENSE; переупаковка (Windows zip; остальные tar.gz; при отсутствии zip в Git Bash — трёхуровневый откат zip → System32 bsdtar → PowerShell) с пересчётом .sha256
  • На Linux при наличии dpkg-deb дополнительно вызывается build-deb.sh для получения .deb (дерево системной установки /usr/lib/yaoxiang + символическая ссылка /usr/bin/yx)

Устаревшие ручные CI ​

Файлы, корректируемые после миграции:

ФайлСтрокДействие
.github/workflows/_build-platforms.yml254Удалить (заменено матрицей сборки cargo-dist)
.github/workflows/release.yml189Сокращается до шлагбаумов + создания тега (сборка/публикация перенесены в dist-release.yml)
.github/workflows/nightly.yml173Сборочная часть меняется на cargo dist build, логика публикации сохраняется
scripts/build/setup.iss~250Сохранить и узаконить (Windows-мастер)
Итого удалено~600

Сохраняется:

  • ci.yml (повседневные fmt + clippy + test + MSRV, не относится к процессу выпуска)
  • _build-wasm.yml (отдельный поток сборки, подключается параллельным job в dist-release.yml)
  • _build-z3-wasm.yml (Z3 для wasm)
  • docs-deploy.yml (развёртывание документации)

Критерии приёмки ​

«Готов к использованию из коробки» — это проверяемо, миграция считается завершённой не когда «старые и новые продукты совпадают», а когда проходят все следующие пункты:

  • На чистой машине (без Rust / без Z3 / без ~/.yaoxiang) распаковка любого платформенного архива, прямой запуск bin/yaoxiang-rs --version успешен — без установки LD_LIBRARY_PATH (rpath работает)
  • Все lib/yaoxiang/std/*.yx в каталоге распаковки читаемы: native-модули как интерфейсные представления сигнатур, уровень .yx как реальные исходники
  • Запуск LSP на демо-проекте из каталога распаковки: автодополнение членов std / переход к определению работают (поиск относительно exe работает)
  • После распаковки в /usr/local (или ~/.yaoxiang) и добавления в PATH согласно официальной инструкции, yx --version успешен из любого каталога
  • apt install yaoxiang (собственный репозиторий) — запускается, apt upgrade отслеживает версию; под символической ссылкой /usr/bin/yx $ORIGIN движка (по реальному пути) попадает на bin/libz3.so
  • curl ... | sh и irm ... | iex в чистой среде — после выполнения yx запускается, PATH настроен
  • yx toolchain install <ver> / default / update работают: сосуществование нескольких версий, yx-toolchain.toml project pin приоритетнее версии по умолчанию, передняя дверь диспетчеризует в правильную версию (rpath и поиск std в дереве версии самосогласованы, нет комбинации «новый движок + старый fmt»)
  • После портативной распаковки yx откатывается на соседний yaoxiang-rs, поведение совпадает с управляемой установкой
  • После установки через Inno Setup структура каталогов полная, PATH работает, удаление возможно
  • Релиз-активы полные: пересобранные пакеты 5 платформ + .sha256 совпадают с фактическим содержимым
  • Тело релиза — вывод generate-commit-list.ts (merge commit changelog полный)
  • Продукт nightly — Pre-release, не затрагивает последний стабильный тег

Компромиссы ​

Преимущества ​

  • Готов к использованию из коробки — портативная распаковка (rpath + общая библиотека в том же каталоге), инсталляторы раскладывают полную структуру каталогов
  • Стандартная библиотека читаема — пользователь читает std как Python Lib/ (жёсткое требование выполнено)
  • Снижение стоимости поддержки — ~600 строк ручного сборочного YAML заменены на cargo-dist + ~80 строк собственного скрипта
  • Кросс-платформенное единообразие — динамическая линковка на всех платформах + общая библиотека в том же каталоге, без исключений
  • Двухуровневая установка — стандартный канал без нового кода (модель Go/Zig); дружественный канал по образцу Rust, четыре точки входа apt / curl / iex / exe разделяют одну структуру продуктов
  • Управление версиями встроено — передняя дверь yx аналогична rustup/Go GOTOOLCHAIN, не повторяет раскол экосистемы, через который Python/Node задним числом подключали pyenv/nvm/pdm; версионная блокировка инструментов — структурная гарантия, а не договорённость

Недостатки и риски ​

  • Поверхность поддержки дружественных каналов — install.sh / install.ps1 (однострочные скрипты, почти не развиваются) + .deb и публикация метаданных apt-репозитория (автоматизировано в release CI) + crate передней двери yx
  • Масштаб изменений от переименования движка — yaoxiang → yaoxiang-rs требует однократной миграции имён CI-артефактов, Inno, тестов и документации (в рамках этапа 5)
  • Порог входа — команде нужно изучить конфигурацию cargo-dist
  • Риск апстрима cargo-dist — в середине 2025 проект останавливался вместе с Axo, в сентябре того же года оригинальный автор возобновил работу и продолжает релизы (0.29 → 0.32+); смягчается фиксацией dist-version + вендорингом сгенерированного в репозиторий с review
  • У cargo-dist нет встроенной поддержки nightly — часть nightly-публикации остаётся ручной

Связь с RFC-014b ​

RFC-014bRFC-037
ОбластьСборка и распространение сторонних пакетовУпаковка и распространение самого компилятора
Инструментyaoxiang build / yaoxiang publishcargo-dist + собственные скрипты
ПродуктFFI-библиотеки сторонних пакетовКомпилятор + стандартная библиотека + тулчейн
Взаимно исключаютНет, дополняютНет, дополняют

Альтернативы ​

АльтернативаПочему не выбрана
Продолжать писать CI вручнуюУже написано ~870 строк, повторяющийся труд, легко забыть DLL
Писать свой инструмент упаковкиНе изобретать колесо, cargo-dist уже зрелый
Только tar.gz без инсталляторовЕдинственный официальный канал — распаковка + PATH; Inno только для Windows-мастера (решено сохранить для китайских пользователей)
Распространение через DockerКомпилятор и языковой тулчейн требуют нативного бинарника, это не контейнерный сценарий
Собственный Homebrew taptap всегда поддерживается сообществом (homebrew-core), собственный — преждевременная потребность; дружественный канал для macOS — curl-скрипт
Отдельный бинарник-менеджер yaoxiangupПрямой аналог rustup, выполнимо; но создаёт второй глагол для пользователя, и порождает симметричный вопрос «должны ли пакетный менеджер/fmt тоже быть отдельными» — разделение на «переднюю дверь» и движок снимает всё разом (отклонено на обсуждении 2026-09-09)
Только самообновление, без нескольких версийСамообновление одной версии не решает потребность в pin разных версий в разных проектах; отсутствие официального управления версиями в Python/Node вынудило экосистему породить pyenv/nvm/pdm — решено встроить (2026-09-09)
Полностью статическая линковка Z3Решено отклонить — каталожное распространение общей библиотеки для внешней системы — её естественная форма; запихнуть в exe и положить рядом в плане «всё равно идёт с пакетом» эквивалентно, но теряется заменяемость
Удалить Inno SetupРешено отклонить — сохранить как Windows-мастер (дополнительный канал)
Родные инсталляторы cargo-distДопущение о плоском бинарнике конфликтует со структурой bin/+lib/, после установки библиотеки не будет
Собственный WiX/MSIБез генератора cargo-dist стоимость выше, чем уже покрыто Inno

Стратегия реализации ​

Этап 1: Изменения на стороне языка (P0) ​

  1. build.rs: единая динамическая линковка + rpath link-arg на всех платформах; copy_dll() расширяется до copy_shared_lib() (so/dylib/dll)
  2. Предварительно сгенерированные в репозитории native интерфейсные представления (src/std/interfaces/), тестовый шлагбаум обеспечивает синхронизацию с StdModule::exports() (подкоманда gen-std решением отменена)
  3. В find_std_interface_file добавлена ветка поиска относительно exe; package init унифицированно выводит в .yaoxiang/vendor/std

Этап 2: Подключение cargo-dist (P0) ​

  1. Запустить cargo dist init для генерации начальной конфигурации (installers = [], dist-version зафиксирован)
  2. Написать package-dist.sh (пересборка + копирование исходников .yx + пересчёт контрольных сумм)
  3. Создать dist-release.yml (запуск по тегу: dist build → пересборка → wasm параллельно → собственная публикация); release.yml сокращается до шлагбаумов + создания тега
  4. Параллельная работа старого и нового пайплайнов, проверка по критериям приёмки

Этап 3: Вывод старого CI (P1) ​

  1. После подтверждения удалить _build-platforms.yml
  2. Сборочная часть nightly.yml заменяется на cargo dist build
  3. setup.iss подключается к новой структуре продуктов (Inno узаконивается; версия инжектируется из Cargo.toml, sed-подстановка устраняется)

Этап 4: Дружественные каналы (P2) ​

  1. install.sh / install.ps1 (определение платформы → скачивание последнего пересобранного пакета → распаковка в versions/ → размещение bin/yx в корне установки → запись settings.toml с версией по умолчанию → подсказка/запись PATH; поставляется вместе с этапом 5 за один раунд, в финальном виде сразу)
  2. Упаковка .deb (повторное использование того же дерева каталогов из package-dist.sh + символическая ссылка /usr/bin) + статический apt-репозиторий на GitHub Pages (метаданные с GPG-подписью, публикуются release CI)

Этап 5: Передняя дверь yx и переименование движка (P2) ​

  1. Текущий монолит переименовывается в yaoxiang-rs (CI-артефакты, Inno, тесты и документация проходят полный цикл, однократная миграция)
  2. Новый маленький crate yx (член workspace): разрешение версий, скачивание релиз-артефактов, распаковка tar/zip, settings.toml, диспетчеризация (оставлены только глаголы toolchain/self, остальные прозрачно транслируются, ручная диспетчеризация без clap для гарантии побайтовой пересылки аргументов); индекс версий из GitHub Releases latest API (зеркало — префиксный конкат в стиле ghproxy); имя команды yx проверено на коллизии (в основных дистрибутивах/Homebrew нет одноимённой частой команды, лишь небольшой инструмент использует yx как алиас)
  3. yx toolchain install/default/update/list/uninstall + project pin yx-toolchain.toml + зеркала + yx self update
  4. Скрипты бутстрапа: curl | sh / irm | iex → скачивают пересобранный пакет, за один раз устанавливают переднюю дверь и стабильную версию по умолчанию (поставляется с этапом 4 за один раунд в финальном виде)

Этап 6: Возможные опции (все неблокирующие) ​

  1. Отправка в winget (указывает на Inno exe, поддерживается сообществом, симметрично модели homebrew-core)
  2. .rpm (для пользователей dnf, аналогично .deb)
  3. Homebrew: после достижения известности до порога допуска в homebrew-core — сообществом
  4. npm @yaoxiang/cli — собственный wrapper (имя в данный момент не зарегистрировано)

Открытые вопросы ​

Нерешённые ​

  • Должен ли уровень .yx предоставлять питоновскую семантику «ручная правка каталога дистрибутива сразу принимается компилятором»? По умолчанию нет — авторитет компиляции остаётся встроенным как в RFC-036 (версия std строго связана с бинарником), каталог дистрибутива — читаемое представление + источник разрешения LSP. Если в будущем открывать, нужно пересматривать инвариант версионной привязки.

Закрытые ​

Следующие вопросы решены в ходе обсуждения дизайна:

  • Возможна ли статическая линковка Z3 на Windows? → Не делать статическую линковку, динамическая на всех платформах (пересмотр 2026-09-09, сохранено)
  • Имя подкоманды gen-std-interfaces? → Подкоманду не заводить (решение 2026-09-10: после того как упаковка сформирована, подкоманда — лишняя поверхность; native интерфейсные представления переведены в предварительно сгенерированные в репозитории src/std/interfaces/ + синхронизация через тестовый шлагбаум, упаковка — чистое копирование)
  • Сохранять ли Inno Setup? → Сохранить как Windows-мастер (дополнительный канал)
  • Физически ли дистрибутив содержит исходники стандартной библиотеки? → Обязательно (читаемость для пользователя по аналогии с Python Lib/, решение 2026-09-09)
  • Родные инсталляторы cargo-dist (shell/powershell/homebrew/msi/npm)? → Все отброшены, инсталляторы собственные (допущение плоского бинарника конфликтует со структурой bin/+lib/)
  • Стратегия инсталляторов? → Двухуровневая (окончательное решение 2026-09-09): стандартный канал = дистрибутив = продукт (модель Go/Zig, распаковка + PATH); дружественный канал по образцу Rust — установка одной строкой: Linux apt (собственный deb-репозиторий) / curl | sh, Windows irm | iex / Inno exe. Не делать: MSI, родные инсталляторы cargo-dist, собственный brew tap
  • Включать ли менеджер версий? → Обязательно, входит в систему инсталляторов (решение 2026-09-09, отменяет границу «отдельный RFC в перспективе» того же дня): мотивация — отсутствие официального управления версиями в Python/Node привело к разделению экосистемы через pyenv/nvm/pdm
  • Форма менеджера версий: отдельный бинарник или подкоманда? → Разделение на «переднюю дверь» и движок (окончательное решение 2026-09-09, три раунда сходимости A→C→инверсия именования): основная команда yx = передняя дверь (маленький бинарник, встроенные глаголы toolchain/self), движок yaoxiang-rs = переименование нынешнего монолита yaoxiang; структура каталогов versions/<ver>/ — версия — понятие первого уровня, каталог версии = корень распаковки дистрибутива, инструменты блокируются версией целиком (старый fmt не распознаёт новый синтаксис; задуманный внутренний toolchains/ отменён из-за избыточности). Прецеденты: Go go передняя дверь + GOTOOLCHAIN, прокси-диспетчеризация rustup
  • Условное выполнение extra-artifacts в cargo-dist? → Обрабатывать скриптом package-dist.sh через case-ветвления в shell
  • Совместимость версий интерфейсов стандартной библиотеки? → Поставляется вместе с версией компилятора, в одном архиве

Ссылки ​