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.iss | Inno 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_64 | x86_64-unknown-linux-gnu | Основная платформа |
| Linux ARM64 | aarch64-unknown-linux-gnu | Кросс-компиляция в CI |
| macOS x86_64 | x86_64-apple-darwin | Intel Mac |
| macOS ARM64 | aarch64-apple-darwin | Apple Silicon |
| Windows x86_64 | x86_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:
// Единая динамическая линковка + 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):
| Язык | Официальный дистрибутив | Официальный способ установки | Кто поддерживает инсталлятор |
|---|---|---|---|
| Go | go/{bin,src,pkg} tarball | Официальная документация = «скачать → распаковать → PATH» | Нет (brew/apt — сообщество) |
| Zig | zig/{bin,lib/std} tarball | То же, без официального скрипта установки | Нет (homebrew-core — сообщество) |
| Node | {bin,lib,include} tarball | tar + официальный pkg/msi | Команда, сама |
| Rust | Многокомпонентный tarball | rustup | Команда, сама |
| Crystal | {bin,src,embedded} tarball | deb/rpm/tar | Команда + brew-сообщество |
| Deno/Bun | Одиночный бинарник zip | Официальный curl-скрипт | Команда, сама (скрипт минимален) |
| Gleam | Одиночный бинарник cargo-dist | Скрипт, сгенерированный cargo-dist | cargo-dist |
Три закономерности:
- Ни один тулчейн с множеством файлов не использует сторонний генератор для инсталляторов — инсталляторы cargo-dist заточены под сценарий одиночного бинарника (Gleam может их использовать именно потому, что это единственный Rust-бинарник без внешних зависимостей)
- Простейшая модель — «дистрибутив = продукт» как у Go/Zig: официальная инструкция — распаковка + PATH, ноль кода инсталлятора; дистрибутив содержит читаемые исходники std (Go
src/, Ziglib/std/, Crystalsrc/) — это норма - Те, кто хотят «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:
- Проект
.yaoxiang/vendor/std/<name>.yx(переопределение проекта, существующее) - Каталог exe
../lib/yaoxiang/std/<name>.yx(новое): портативная распаковка, управляемая установка (versions/<ver>/), deb — всё единообразно попадает ~/.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/cli | Wrapper для скачивания дистрибутива | Отложено: npm wrapper cargo-dist также основан на допущении плоского артефакта, отбрасывается вместе с инсталляторами; если нужен канал npm, пишется свой wrapper (скачивает и распаковывает пересобранный пакет, та же логика что и распаковка при установке) |
@yaoxiang/playground | wasm-библиотека (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, публикацию оставить как есть.
- Три job check-version / security / test остаются как есть (триггер — push main, шлагбаумы перед созданием тега)
- После прохождения всех трёх job release создаёт и пушит тег
v{version}(без изменений) - 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) - publish job:
generate-commit-list.tsгенерирует тело (текущий скрипт переиспользуется) → добавляет загрузку пересобранных пакетов +.sha256+.deb+ wasm + Setup exe; отдельныйpublish-aptjob публикует метаданные 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 привязан к тегу, сборка и публикация в нём связаны):
# 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 в репозитории)
[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.yml | 254 | Удалить (заменено матрицей сборки cargo-dist) |
.github/workflows/release.yml | 189 | Сокращается до шлагбаумов + создания тега (сборка/публикация перенесены в dist-release.yml) |
.github/workflows/nightly.yml | 173 | Сборочная часть меняется на 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.socurl ... | shиirm ... | iexв чистой среде — после выполненияyxзапускается, PATH настроенyx toolchain install <ver>/default/updateработают: сосуществование нескольких версий,yx-toolchain.tomlproject 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-014b | RFC-037 | |
|---|---|---|
| Область | Сборка и распространение сторонних пакетов | Упаковка и распространение самого компилятора |
| Инструмент | yaoxiang build / yaoxiang publish | cargo-dist + собственные скрипты |
| Продукт | FFI-библиотеки сторонних пакетов | Компилятор + стандартная библиотека + тулчейн |
| Взаимно исключают | Нет, дополняют | Нет, дополняют |
Альтернативы
| Альтернатива | Почему не выбрана |
|---|---|
| Продолжать писать CI вручную | Уже написано ~870 строк, повторяющийся труд, легко забыть DLL |
| Писать свой инструмент упаковки | Не изобретать колесо, cargo-dist уже зрелый |
| Только tar.gz без инсталляторов | Единственный официальный канал — распаковка + PATH; Inno только для Windows-мастера (решено сохранить для китайских пользователей) |
| Распространение через Docker | Компилятор и языковой тулчейн требуют нативного бинарника, это не контейнерный сценарий |
| Собственный Homebrew tap | tap всегда поддерживается сообществом (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)
build.rs: единая динамическая линковка + rpath link-arg на всех платформах;copy_dll()расширяется доcopy_shared_lib()(so/dylib/dll)- Предварительно сгенерированные в репозитории native интерфейсные представления (
src/std/interfaces/), тестовый шлагбаум обеспечивает синхронизацию сStdModule::exports()(подкоманда gen-std решением отменена) - В
find_std_interface_fileдобавлена ветка поиска относительно exe;package initунифицированно выводит в.yaoxiang/vendor/std
Этап 2: Подключение cargo-dist (P0)
- Запустить
cargo dist initдля генерации начальной конфигурации (installers = [],dist-versionзафиксирован) - Написать
package-dist.sh(пересборка + копирование исходников .yx + пересчёт контрольных сумм) - Создать
dist-release.yml(запуск по тегу: dist build → пересборка → wasm параллельно → собственная публикация);release.ymlсокращается до шлагбаумов + создания тега - Параллельная работа старого и нового пайплайнов, проверка по критериям приёмки
Этап 3: Вывод старого CI (P1)
- После подтверждения удалить
_build-platforms.yml - Сборочная часть
nightly.ymlзаменяется наcargo dist build setup.issподключается к новой структуре продуктов (Inno узаконивается; версия инжектируется из Cargo.toml, sed-подстановка устраняется)
Этап 4: Дружественные каналы (P2)
install.sh/install.ps1(определение платформы → скачивание последнего пересобранного пакета → распаковка вversions/→ размещениеbin/yxв корне установки → записьsettings.tomlс версией по умолчанию → подсказка/запись PATH; поставляется вместе с этапом 5 за один раунд, в финальном виде сразу)- Упаковка
.deb(повторное использование того же дерева каталогов изpackage-dist.sh+ символическая ссылка/usr/bin) + статический apt-репозиторий на GitHub Pages (метаданные с GPG-подписью, публикуются release CI)
Этап 5: Передняя дверь yx и переименование движка (P2)
- Текущий монолит переименовывается в
yaoxiang-rs(CI-артефакты, Inno, тесты и документация проходят полный цикл, однократная миграция) - Новый маленький crate
yx(член workspace): разрешение версий, скачивание релиз-артефактов, распаковка tar/zip, settings.toml, диспетчеризация (оставлены только глаголы toolchain/self, остальные прозрачно транслируются, ручная диспетчеризация без clap для гарантии побайтовой пересылки аргументов); индекс версий из GitHub Releases latest API (зеркало — префиксный конкат в стиле ghproxy); имя командыyxпроверено на коллизии (в основных дистрибутивах/Homebrew нет одноимённой частой команды, лишь небольшой инструмент использует yx как алиас) yx toolchain install/default/update/list/uninstall+ project pinyx-toolchain.toml+ зеркала +yx self update- Скрипты бутстрапа:
curl | sh/irm | iex→ скачивают пересобранный пакет, за один раз устанавливают переднюю дверь и стабильную версию по умолчанию (поставляется с этапом 4 за один раунд в финальном виде)
Этап 6: Возможные опции (все неблокирующие)
- Отправка в winget (указывает на Inno exe, поддерживается сообществом, симметрично модели homebrew-core)
.rpm(для пользователей dnf, аналогично.deb)- Homebrew: после достижения известности до порога допуска в homebrew-core — сообществом
- 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-мастер (дополнительный канал)Физически ли дистрибутив содержит исходники стандартной библиотеки?→ Обязательно (читаемость для пользователя по аналогии с PythonLib/, решение 2026-09-09)Родные инсталляторы cargo-dist (shell/powershell/homebrew/msi/npm)?→ Все отброшены, инсталляторы собственные (допущение плоского бинарника конфликтует со структурой bin/+lib/)Стратегия инсталляторов?→ Двухуровневая (окончательное решение 2026-09-09): стандартный канал = дистрибутив = продукт (модель Go/Zig, распаковка + PATH); дружественный канал по образцу Rust — установка одной строкой: Linuxapt(собственный deb-репозиторий) /curl | sh, Windowsirm | 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/отменён из-за избыточности). Прецеденты: Gogoпередняя дверь + GOTOOLCHAIN, прокси-диспетчеризация rustupУсловное выполнение extra-artifacts в cargo-dist?→ Обрабатывать скриптомpackage-dist.shчерез case-ветвления в shellСовместимость версий интерфейсов стандартной библиотеки?→ Поставляется вместе с версией компилятора, в одном архиве
